Sources and updates
MacSoftware and WindowsSoftware each have one source. BuildMacPkg has named
inputs. Both use the same resolvers and lockfile. Windows accompanying files use
these resolvers too.
GitHub, Homebrew, WinGet and direct HTTP can identify the same software. Choose the source that preserves the intended release stream and deployment artifact. Official GitHub Releases already provide structured upstream metadata; routing them through Homebrew is not inherently better. Homebrew and WinGet are especially useful when registry metadata turns mutable or awkward vendor discovery into a concrete version, installer and digest. Do not change channel, architecture, edition or artifact type merely to use a registry. A stable vendor URL remains a good source.
Download a file
source:
url: https://dl.google.com/dl/chrome/install/googlechromestandaloneenterprise64.msi
The filename comes from Content-Disposition, then the final URL's basename, then
the original URL's basename. It is pinned in the lockfile. Set filename when a
vendor endpoint returns an unusable name:
source:
url: https://vendor.example/download
filename: VendorSetup.exe
headers:
User-Agent: VendorDeploymentClient
token: "{{ env.VENDOR_DOWNLOAD_TOKEN }}"
token supplies a bearer token. It cannot be combined with an Authorization
header. Credentials and custom headers stay confined to the source origin across
redirects. An optional sha256 asserts a known digest in the declaration.
Pair it with filename to resolve without downloading. Otherwise the first
fetch discovers the filename, which later updates can reuse with that digest.
The lock keeps only a digest of this declaration, so a URL that works as a
credential can come from {{ env.NAME }}. Set filename with it; the default
name can come from the URL.
Discover a release
GitHub release assets use the github resolver:
source:
resolver: github
repository: company/application
release: latest
asset: Application-*-arm64.zip
Replace the repository and asset pattern with the publisher's values. asset is
an asset-name glob using the same doublestar syntax as local input patterns, not
a regular expression. Exact names work too. The pattern must match exactly one
asset in the selected release; zero or multiple matches fail. For example,
SafeExamBrowser-*.dmg selects SafeExamBrowser-3.7.1.dmg without declaring a version.
release: latest and an omitted or empty release select GitHub's latest release.
A value containing *, ? or [ is a tag glob with the same syntax as asset:
discovery selects the newest published release whose tag matches, so
release: v3.* follows one release line. Any other value selects that exact
release tag. Draft releases are rejected.
For GitHub, inputs.<name>.version is the raw release tag, such as v1.2.3 or
Audacity-3.7.3. Select or transform it explicitly when a package requires a
numeric version.
Set include_prereleases: true to include prereleases: latest selects the newest
published release across both stable releases and prereleases, and a tag glob also
matches prereleases. Tag globs and prerelease discovery compare published_at
across all pages, skip drafts, and use the higher release ID to break equal
timestamps. An explicit release tag still selects that exact release. Asset
matching applies to the selected release; a missing asset does not fall back to an
older release. The lock continues to pin the concrete release and asset identities.
source:
resolver: github
repository: bambulab/BambuStudio
asset: Bambu_Studio_mac_*.dmg
include_prereleases: true
GitHub allows 60 unauthenticated release lookups an hour from one address. When
GH_TOKEN, or else GITHUB_TOKEN, is set, lookups for sources without a token
use it and get the authenticated limit. That token goes to the GitHub API only;
downloads never carry it. A source's own token is used for both.
For an HTTP page containing a download link, use resolver: http with url and a
match regular expression. HTML pages match on element attribute values, so links
in comments, page text and script content are ignored; a plain-text release feed
matches on the response body. The full match can be an absolute, protocol-relative, root-relative or path-relative
URL. References resolve against the page's base URL: the final URL after
redirects, or the base element when the page declares one. Equivalent references count as one URL. All matches must resolve to one distinct
stable HTTP(S) URL. The lock stores that absolute URL, so locked fetches do not
repeat discovery or reapply the pattern. Credentials remain confined to the
configured source origin. Accept, Accept-Encoding, Accept-Language and
User-Agent affect source identity. Other headers are treated as credentials,
so rotating them does not stale the lock. Discovery belongs to the resolver; the software kind still receives
one file. External plugins can supply other resolvers.
A page that keeps older releases beside the current one matches several URLs.
Name a version group in the pattern to select the highest:
source:
url: https://vendor.example/downloads
match: https://vendor\.example/files/App-(?P<version>[0-9.]+)\.dmg
The group captures dotted numbers such as 6.3.5, compared number by number, so
10.2 is above 9.12 and 2.0 equals 2.0.0. Any other capture fails, so keep
betas out with the pattern. Two different URLs at the highest version fail too. The
captured version becomes the input's version, as a GitHub release tag does.
An input's version is evidence about the source. A build reads it as
inputs.<name>.version, and destination metadata as evidence.source.version.
Mac software takes its managed version from the installer or the selected
application, never from the source, so the two can differ; a declaration that
wants the source's version says so, as in
version: "{{ evidence.source.version }}".
Follow Homebrew releases
Homebrew casks and bottles provide download hashes, so stemma update checks for
new releases without downloading installers, even with an empty cache. prepare
downloads the selected file when needed and verifies its hash.
source:
resolver: homebrew
cask: vlc
Stemma reads cask and formula metadata to acquire artifacts. It does not execute Homebrew install, uninstall or postflight actions.
A cask selects the vendor download. architecture defaults to arm64 and
macos to the current supported release, macOS 27. Both describe the target;
runner architecture and OS do not participate. Set architecture: x86_64 or a
supported macOS major version explicitly when needed. language selects a cask
language alias; omission uses its declared default.
Use formula: mole instead of cask to select a Homebrew core bottle. Formula
sources require any_skip_relocation, no runtime formula dependencies and no
post-install actions. An older bottle of the same architecture can satisfy a
newer target. There is no source-build fallback or Homebrew prefix emulation.
A cask may supply a universal or Intel artifact for an Apple silicon target.
Hashless and no_check casks still check their vendor bytes during update.
A bottle remains a source archive. Use BuildMacPkg to declare the installed
layout, including support files and command links, then publish that package
with MacSoftware. MacSoftware requires an application or installer; it does
not choose an installation location for a command-line tool.
In a build, inputs.<name>.version includes the upstream version, formula
revision and bottle rebuild, for example 1.56.1.0.0. $input selects the
formula's content root, so path: libexec works without knowing the bottle's
archive layout. Omitting path copies that whole root. Detailed Homebrew claims
remain available in inputs.<name>.evidence.
Follow WinGet releases
WinGet also supplies installer hashes: checking for updates does not require downloading the installer.
source:
resolver: winget
package: Google.Chrome
architecture: x64
scope: machine
installer_type: wix
The community source supplies the latest version by default. version can select
an exact published version. Version selection happens before installer filtering;
a missing match never falls back to an older release. Architecture defaults to
x64 independently of the runner. scope, installer_type and locale filter
explicit manifest claims. Exactly one installer must match. An absent installer
locale remains absent; the package description's locale does not fill it.
The lock retains the selected installer and its declared settings under
winget.installer evidence. Stemma does not run the WinGet client or install its
dependencies. Store-only, authenticated and download-prohibited installers are
unsupported.
Use local files
source:
path: Assets/Vendor.pkg
path can select a file or directory. Relative paths resolve from the resource
file, not the shell's working directory. An absolute path names a location on the
host running the command:
source:
path: /Applications/GarageBand.app
Host paths are machine-specific, so the content identity in the lockfile is what travels. Symlinks behave as they do everywhere else: a link resolves to its target. To snapshot selected files from a directory:
source:
resolver: local
base: Assets
include:
- "**/*.ttf"
- "**/*.otf"
include uses doublestar globs; each pattern must match at least one entry.
Overlapping patterns select each entry once. base defaults to the resource file's
directory and stays inside the project, because its contents are walked without
following symlinks. file.path is an exact path, not a glob. Native resolvers
reject fields belonging to another resolver.
Files and trees have content identities. Tree identity also includes permission modes and supported symlinks. Packaging cannot silently discard required metadata; see filesystem limits.
Use a build output
source:
resource:
apiVersion: stemma/v1alpha1
kind: BuildMacPkg
name: fonts
output: installer
References include the kind and name, plus the named output. apiVersion defaults
to stemma/v1alpha1; supply it for external kinds. Required builds run before
their consumers even when only the consumer was selected on the command line.
A MacSoftware resource has an installer output as well: the installer it
selected. A build can take it as an input.
Understand the lock
| Command | Input behaviour |
|---|---|
update | Discover current inputs and update their lock documents; does not publish |
prepare | Require matching locked declarations and local inputs; prepare artifacts |
plan / apply | Require matching locked declarations and local inputs; fetch only the recorded remote observation |
The lock is the stemma.lock/ directory beside the Project. Each resource's
inputs are locked in a document of their own, stemma.lock/<Kind>/<name>.yaml,
and the plugins in stemma.lock/plugins.yaml; a kind of another API group keeps
its apiVersion in the path. A run rewrites only the documents of the resources
it changed, so a commit can take one software's declaration with its lock
document, and changes to different resources never edit the same file. Delete a
resource's lock document with its declaration.
A lock records the resolver and its version, the relevant declaration, a resolver-owned observation and content identity. It is not merely a version number or download URL. GitHub locks retain the selected release ID, asset ID, release tag, download URL, filename and digest. Locked fetches replay that concrete selection without evaluating the asset glob or looking up the latest release again. Changing the pattern or release selector makes the declaration stale and requires a lock update, even with cached bytes.
Homebrew, WinGet and GitHub assets with published hashes can update the lock without downloading the file. The hash pins the required bytes; preparation verifies them before use. A changed version or installer setting can therefore appear in review even when the file itself is unchanged.
update also avoids downloads the cache or the lock can answer. A GitHub asset never
changes, so an asset the cache fetched before, or the one the lock records, is
not downloaded again. An HTTP URL can serve new bytes at any time: after
rediscovering a match link, update asks the server with the ETag and
Last-Modified the cache kept from its last download of that URL, and downloads
when the answer is new or the server sends no validators. The cache remembers
what it fetched whichever lock is checked out; the lock only decides whether
the result is a change. Runs that prepare fetch any content the cache no longer
holds.
Resources execute independently. Acquisition or preparation failures leave that
resource's complete reviewed lock entries unchanged; consumers of its outputs
are reported as blocked, with the resources that blocked them. Successful
resources can still update their entries. update can therefore write the
lockfile and exit nonzero after reporting every resource. A failed refresh stays
a failure even when the previous locked bytes remain cached.
stemma update Kind/name and stemma update --profile NAME refresh the selected
resources and the builds they reference. Every other entry stays as reviewed, so
the lock diff holds only what the run selected. An update without a selection
also removes the entries of resources that are no longer declared.
A registry hash can be stale: resolution may succeed and preparation then fail. Every consuming path verifies the required digest before using content. Offline preparation requires cached bytes, not merely a resolved lock.
If a vendor replaces bytes at a stable URL, a cold locked run fails the content
check. It does not silently accept today's download. Run update and review the
change, or restore the locked bytes to the cache from a trusted copy.
Keep stemma.lock/ in Git. Destination descriptions, categories and other
metadata edits do not invalidate source acquisition or rebuild unchanged content.
Cache and offline runs
stemma cache info
stemma prepare --offline
--offline requires cached network inputs and still checks local files.
It controls source and plugin acquisition; it does not disable destination
network access for plan or apply.
Reusing prepared outputs verifies their recorded identity and inspection results. Destinations can reconcile metadata without installer bytes. Content is copied and verified when a consumer needs it; corrupt cached content is discarded and the command fails. Retry to fetch or rebuild it. Local inputs are still read in full to compare their current content with the lock.
Repeated source lookups share cached responses and check with the publisher for changes on later runs. A failed lookup remains a failure, even with cached data.
The disposable cache lives under stemma in the system's user cache directory
by default, outside the catalog. stemma cache path prints the effective location.
Set STEMMA_CACHE_DIR or --cache-dir when a runner needs another location, such
as a mounted cache volume. A repository-local cache is optional; ignore its path
in Git if you choose one.
Cache-using commands maintain the cache at startup and completion. Automatic
maintenance skips collection while another run holds a cache lease. A complete
reconcile invocation shares one lifecycle across all its phases; MCP tools
maintain the cache per call. An idle machine does no maintenance.
--cache-max-size / STEMMA_CACHE_MAX_SIZE sets the retained-cache budget,
defaulting to 32GiB; 0 disables automatic content eviction. The budget includes
downloads, prepared objects, plugins, materialized copies and disposable
metadata. Under size pressure, least recently used entries are removed first.
Successful reuse refreshes recency; inspection does not. Entries used within the
last 24 hours are protected. This is a soft target: recent content and temporary
work can exceed it. Abandoned work and partial writes are recovered only when no
run holds a lease, independently of size pressure.
stemma cache info shows the effective policy and a logical file-size breakdown.
stemma cache prune applies the policy, waiting for active runs to finish;
--dry-run previews the removals. stemma cache prune --all deliberately removes
all disposable content, including recent entries. Prune reports reclaimed bytes
and retained usage; both commands accept --json. Automatic maintenance reports
reclaimed space, failures and an unmet size target on stderr, without changing
the command's outcome.
Explicitly offline commands skip automatic content eviction, but still clean up
temporary work. This cannot restore content an earlier online run evicted.
Locked remote inputs can be fetched again only if the publisher still serves the
recorded bytes; acquisition verifies those bytes without resolving a newer
version or rewriting the lock. Prepared outputs depend only on input content,
configuration and the stemma or plugin build, so a new or recreated lock reuses
them. The cache is not an artifact archive. Use artifact --output-file PATH for
a durable export outside it.
Destinations keep no local state either: each one identifies its publications itself. See publication identity and retention.