Repo plugins that let admins push packages directly into
UVIT using munkiimport and AutoPkg's
MunkiImporter processor. Two plugins, same API and configuration:
UVITRepo.plugin(Swift) — for Munki 7's Swift tools (munkiimportetc.), which load dylib plugins from/usr/local/munki/repoplugins/. Requires Munki 7.0 or later.UVITRepo.py(Python) — for AutoPkg'sMunkiImporterprocessor, which loads Munki's Python repo plugins (munkilib.munkirepo, still shipped by Munki 7.x as the Munki 6.7 compatibility component). Installed at/usr/local/munki/munkilib/munkirepo/UVITRepo.py.
Download the latest UVITRepo-<version>.pkg from the
Releases page
and install it:
sudo installer -pkg UVITRepo-1.0.0.pkg -target /
This installs UVITRepo.plugin into /usr/local/munki/repoplugins/ and
UVITRepo.py into /usr/local/munki/munkilib/munkirepo/.
Log in to the UVIT console and go to My Account → API Tokens → Create Token
(/myaccount/tokens). Copy the generated token — it looks like uvit_pat_….
This is your UVIT_TOKEN.
The token identifies you as a user. The server checks your membership for the
target you specify (UVIT_TARGET); it does not read a tenant or repo from the
token itself.
UVIT_TARGET:globalif you are a sysadmin pushing to the global scope, ortenant:<id>(e.g.tenant:42) for a specific tenant. The server uses this value to resolve the tenant and verify that the token owner is a member. Requests without a target are rejected with 400.UVIT_REPO_ID: the numeric ID of the Software Repo to write to. Find it in the UVIT console under Admin > Software Repos — the ID is shown in the repo detail view.
munkiimport --configure
Set the following values when prompted:
| Setting | Value |
|---|---|
repo_url | https://<your-console>/api/repo |
plugin | UVITRepo |
Example:
repo_url = https://console.uvit.eigercode.ch/api/repo
plugin = UVITRepo
Option A — environment variables (session-scoped):
export UVIT_TOKEN="uvit_pat_..."export UVIT_TARGET="tenant:42"export UVIT_REPO_ID="7"
munkiimport GoogleChrome.dmgOption B — macOS admin preferences (persistent, recommended for workstations):
sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitToken "uvit_pat_..."
sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitTarget "tenant:42"
sudo defaults write /Library/Preferences/ch.eigercode.uvit.munkiimport \
uvitRepoID "7"Read them back to verify:
sudo defaults read /Library/Preferences/ch.eigercode.uvit.munkiimportmunkiimport /path/to/GoogleChrome.dmgmunkiimport will:
- Upload the installer to the UVIT S3 repo (
PUT /api/repo/pkgs/…). - Upload the generated pkginfo plist (
PUT /api/repo/pkgsinfo/…). - Call
makecatalogs— this is a no-op on the UVIT side (catalogs are built dynamically from the database).
The package appears in the UVIT console immediately after a successful push.
AutoPkg's MunkiImporter does not use the Swift plugin — it loads Munki's
Python repo plugins, so it uses UVITRepo.py (installed by the same pkg; on a
bare CI runner just copy the file into
/usr/local/munki/munkilib/munkirepo/). Munki tools must be installed too:
MunkiImporter shells out to /usr/local/munki/makepkginfo, and the Python
munkilib comes with Munki's compatibility component package.
Add these variables to your AutoPkg preferences or pass them on the command line:
<key>MUNKI_REPO</key>
<string>https://console.uvit.eigercode.ch/api/repo</string>
<key>MUNKI_REPO_PLUGIN</key>
<string>UVITRepo</string>Set the UVIT variables in the environment before running AutoPkg:
export UVIT_TOKEN="uvit_pat_..."export UVIT_TARGET="tenant:42"export UVIT_REPO_ID="7"
autopkg run com.github.autopkg.recipe.GoogleChromePkg.munkiFor unattended CI runs (GitHub Actions + cloud-autopkg-runner) see the uvit-autopkg repo.
| Env var | Pref key | Required | Description |
|---|---|---|---|
UVIT_TOKEN | uvitToken | Yes | Opaque API token from My Account → API Tokens (uvit_pat_…). |
UVIT_TARGET | uvitTarget | Yes | global or tenant:<id>. Sent on every request. The server |
| resolves the tenant from this value and checks membership. | |||
UVIT_REPO_ID | uvitRepoID | For writes | Numeric repo ID; required for PUT/DELETE on pkgs/pkgsinfo. |
Preferences domain: ch.eigercode.uvit.munkiimport
| Munki call | UVIT API endpoint | Headers |
|---|---|---|
list("pkgsinfo") | GET <repo_url>/pkgsinfo | Auth + Target |
list(<other kind>) | returns [] (see Known Limitations below) | — |
get("pkgsinfo/<name>/<ver>.plist") | GET <repo_url>/pkgsinfo/<name>/<ver>.plist | Auth + Target |
get("pkgs/<path>") | GET <repo_url>/pkgs/<path> → 307 to S3 | Auth + Target |
put("pkgs/<path>", fromFile:) | PUT <repo_url>/pkgs/<path> → 307 to S3 | Auth + Target + Repo-ID |
put("pkgsinfo/<path>", content:) | PUT <repo_url>/pkgsinfo/<path> | Auth + Target + Repo-ID |
delete("pkgsinfo/<path>") | DELETE <repo_url>/pkgsinfo/<path> | Auth + Target + Repo-ID |
delete("pkgs/<path>") | DELETE <repo_url>/pkgs/<path> | Auth + Target + Repo-ID |
pathFor(_) | nil (non-filesystem repo) | — |
makecatalogs (called by Munki tools) | POST <repo_url>/makecatalogs → no-op 200 | Auth + Target |
Header names: Authorization: Bearer <token>, X-UVIT-Target: <target>,
X-UVIT-Repo-ID: <repoID>.
Both plugins follow a 307 Temporary Redirect on PUT pkgs/<path> to a
presigned S3 PUT URL and re-send the file bytes there, without forwarding the
Authorization/X-UVIT-* headers (the presigned URL carries its own auth).
An older console that predates this (pre-uvit-console#575) never sends a
redirect on that PUT, so both plugins keep working against it unchanged.
This affects only UVITRepo.plugin loaded by Munki's Swift tools. AutoPkg
runs are unaffected — they use the Python plugin, which involves no dylib
loading.
macOS Library Validation requires that a dylib loaded by a hardened process
either shares the same Apple Developer Team ID as the host binary, or that
the host binary carries the
com.apple.security.cs.disable-library-validation entitlement.
Munki 7 binaries are signed by the Munki project (Greg Neagle / googlemunki). UVITRepo.plugin is signed by EigerCode GmbH (different Team ID). Whether macOS allows the load depends on Munki's exact entitlements.
This must be tested on a real Mac running a production Munki 7 build before deploying to a fleet. If Library Validation blocks the load, options are:
- File a Munki issue requesting
com.apple.security.cs.disable-library-validationbe added to Munki's entitlements (the correct long-term fix). - Use an ad-hoc or unsigned build of the plugin for internal testing only.
list("catalogs"), list("manifests"), and list("icons") return empty
lists. This means:
makecatalogs(separate Munki tool) cannot enumerate catalogs via the API — it is not needed here because UVIT builds catalogs dynamically.iconimportercannot sync icons via this API.
These require server-side list endpoints that are planned as a follow-up.
Requires Xcode 16+ and macOS 12+.
xcodebuild build \
-project UVITRepo.xcodeproj \
-configuration Release \
-scheme UVITRepo \
-destination "generic/platform=macOS" \
-derivedDataPath buildOr use the convenience script (also creates a .pkg):
./build_pkg.shMIT — see LICENSE.