{"description":"Exploring modern cloud workflows. DevOps, APIs, Git, serverless architectures, pipelines, and software integrations, alongside personal tech solutions.","feed_url":"https://focused-systems.pages.dev/feed.json","home_page_url":"https://focused-systems.pages.dev/","items":[{"authors":[{"name":"Rod Christiansen"}],"content_html":"\u003cp\u003e\u003cimg src=\"https://github.com/rodchristiansen/asbmutil/raw/main/resources/main.png?raw=true\" alt=\"ASBMUtil app\"\u003e\u003cp\u003eWhen Apple finally released the API for Apple Business \u0026amp; School Manager last year I was thrilled. I took it as an opportunity to try and create a Swift CLI for it which ended up being \u003ca href=\"https://github.com/rodchristiansen/asbmutil\"\u003easbmutil\u003c/a\u003e, a Swift binary that directly communicates with the \u003ca href=\"https://developer.apple.com/documentation/apple-school-and-business-manager-api\"\u003eApple School \u0026amp; Business Manager API\u003c/a\u003e. I posted it in the MacAdmins Slack, didn't think much about it, and it ended up getting used by folks — along with \u003ca href=\"https://github.com/rodchristiansen/asbmutil/issues?q=is%3Aissue%20state%3Aclosed\"\u003erequests to make it better\u003c/a\u003e.\u003c/p\u003e\u003cp\u003eToday I'm shipping \u003cstrong\u003eASBMUtil.app\u003c/strong\u003e, a native SwiftUI front-end on top of the same engine. Same credentials store, same API client, same bulk operations — inside a native Liquid Glass Tahoe app.\u003c/p\u003e\u003cfigure class=\"kg-card kg-image-card\"\u003e\u003cimg src=\"ASBMUtil-2-1.png\" class=\"kg-image\" alt=\"ASBMUtil app\" loading=\"lazy\" width=\"256\" height=\"256\"\u003e\u003c/figure\u003e\u003cp\u003eNot every Mac admin wants to live in the terminal. Even a well-built CLI carries its own friction — \u0026quot;here's the command, make sure the profile is set, redirect jq somewhere, don't forget the \u003ccode\u003e\u0026ndash;mdm\u003c/code\u003e flag…\u0026quot;\u003c/p\u003e\u003cp\u003eHence the GUI app. Same engine underneath, same credentials store, same API client.\u003c/p\u003e\u003cfigure class=\"kg-card kg-image-card kg-width-wide\"\u003e\u003cimg src=\"https://github.com/rodchristiansen/asbmutil/raw/main/resources/bulk.png?raw=true\" class=\"kg-image\" alt=\"ASBMUtil app\" loading=\"lazy\" width=\"4052\" height=\"2210\"\u003e\u003c/figure\u003e\u003ch2 id=\"whats-in-the-app\"\u003eWhat's in the App\u003c/h2\u003e\u003cp\u003eThe main view is a single native table with every managed device in your Apple Business \u0026amp; School Manager account — Macs, iPads, iPhones, Apple TVs, all of it — right next to each other. Click a row and a sidebar slides in with the full device details: serial, order, status, MDM assignment, AppleCare coverage, MAC addresses, the works. Select a handful, right-click, reassign.\u003c/p\u003e\u003cul\u003e\u003cli\u003e\u003cstrong\u003eDevice browser\u003c/strong\u003e — every managed device in one paginated, sortable table with a details sidebar. AppleCare coverage enrichment is a toggle away.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003ePowerful filters\u003c/strong\u003e — filter by device status, by order number, by model family, by MDM server — stack filters to narrow to exactly the specific devices you want.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eMDM server list and assignments\u003c/strong\u003e — see every server and what's assigned to it.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eBulk assign / unassign\u003c/strong\u003e — multi-select in the table, pick a destination server, go. Or import a CSV if that's how your desired state arrives.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eExport to CSV or JSON\u003c/strong\u003e — selection or filtered results out through the macOS share sheet.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eMulti-profile switching\u003c/strong\u003e — flip between Apple Business \u0026amp; School Manager instances from the sidebar; manage credentials in settings.\u003c/li\u003e\u003c/ul\u003e\u003cfigure class=\"kg-card kg-gallery-card kg-width-wide\"\u003e\u003cdiv class=\"kg-gallery-container\"\u003e\u003cdiv class=\"kg-gallery-row\"\u003e\u003cdiv class=\"kg-gallery-image\"\u003e\u003cimg src=\"https://github.com/rodchristiansen/asbmutil/raw/main/resources/profiles.png?raw=true\" width=\"1344\" height=\"782\" loading=\"lazy\" alt=\"ASBMUtil app\"\u003e\u003c/div\u003e\u003cdiv class=\"kg-gallery-image\"\u003e\u003cimg src=\"https://github.com/rodchristiansen/asbmutil/raw/main/resources/export.png?raw=true\" width=\"604\" height=\"504\" loading=\"lazy\" alt=\"ASBMUtil app\"\u003e\u003c/div\u003e\u003c/div\u003e\u003c/div\u003e\u003c/figure\u003e\u003ch2 id=\"filters\"\u003eFilters\u003c/h2\u003e\u003cp\u003eThe filters match the web — same categories, same stacking behaviour — so if you already use the ABM filters, you already know how these work. The difference is that here they run against a native table with every device already loaded, so stacking is instant, the selection carries into bulk assign/unassign in one click, and the filtered set exports to CSV or JSON through the share sheet.\u003c/p\u003e\u003cp\u003eFilter by order number, device status, model family, or MDM server — and stack them, so \u003cem\u003e\u0026quot;unassigned iPads from the last PO that haven't been routed to Intune yet\u0026quot;\u003c/em\u003e is three clicks.\u003c/p\u003e\u003cfigure class=\"kg-card kg-gallery-card kg-width-wide\"\u003e\u003cdiv class=\"kg-gallery-container\"\u003e\u003cdiv class=\"kg-gallery-row\"\u003e\u003cdiv class=\"kg-gallery-image\"\u003e\u003cimg src=\"https://github.com/rodchristiansen/asbmutil/blob/main/resources/filter_orders.png?raw=true\" width=\"934\" height=\"796\" loading=\"lazy\" alt=\"ASBMUtil app\"\u003e\u003c/div\u003e\u003cdiv class=\"kg-gallery-image\"\u003e\u003cimg src=\"https://github.com/rodchristiansen/asbmutil/blob/main/resources/filter_status.png?raw=true\" width=\"936\" height=\"792\" loading=\"lazy\" alt=\"ASBMUtil app\"\u003e\u003c/div\u003e\u003c/div\u003e\u003c/div\u003e\u003c/figure\u003e\u003cp\u003eOnce you've narrowed to the right cohort, export (CSV/JSON) or bulk-reassign works on the filtered selection — not on the whole fleet.\u003c/p\u003e\u003ch2 id=\"what-you-get\"\u003eWhat You Get\u003c/h2\u003e\u003cul\u003e\u003cli\u003e\u003cstrong\u003ePure Swift 6 binary\u003c/strong\u003e — no external runtime dependencies.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eCredentials stored in the macOS Keychain\u003c/strong\u003e — data-protection class, no per-app ACL prompts.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eMultiple profile support\u003c/strong\u003e — manage several AxM instances (school-district-east, business-unit-3, whatever) from a dropdown instead of \u003ccode\u003e\u0026ndash;profile\u003c/code\u003e flags.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eAutomatic OAuth 2 client-assertion handling\u003c/strong\u003e — token lifecycle handled for you.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003ePaginated device fetch\u003c/strong\u003e — walks the full inventory without you worrying about page cursors.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eBulk operations\u003c/strong\u003e — CSV import \u003cem\u003eor\u003c/em\u003e multi-select directly in the GUI table.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003e\u003ccode\u003eStrictConcurrency\u003c/code\u003e enabled\u003c/strong\u003e — actor-isolated, race-free by design.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eBulk device-to-server resolution\u003c/strong\u003e — server-side device listing gets the whole fleet in 4–5 API calls regardless of size, instead of per-device lookups.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eBulk AppleCare enrichment\u003c/strong\u003e — \u003ccode\u003elist-devices \u0026ndash;include-applecare\u003c/code\u003e runs a two-pass fan-out across the whole fleet, no CSV required.\u003c/li\u003e\u003c/ul\u003e\u003ch2 id=\"recent-api-coverage\"\u003eRecent API Coverage\u003c/h2\u003e\u003cp\u003eI've kept the tool caught up with the AxM API as Apple has shipped new fields:\u003c/p\u003e\u003cul\u003e\u003cli\u003e\u003cstrong\u003eAPI 1.5\u003c/strong\u003e — MAC addresses now support multiple values (array format) for devices with multiple network interfaces.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eAPI 1.4\u003c/strong\u003e — Wi-Fi, Bluetooth, and built-in Ethernet MAC addresses for macOS.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eAPI 1.3\u003c/strong\u003e — AppleCare coverage lookup for devices (single-serial via \u003ccode\u003eget-devices-info\u003c/code\u003e, whole-fleet via \u003ccode\u003elist-devices \u0026ndash;include-applecare\u003c/code\u003e).\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eAPI 1.2\u003c/strong\u003e — Wi-Fi and Bluetooth MAC addresses for iOS, iPadOS, tvOS, and visionOS.\u003c/li\u003e\u003c/ul\u003e\u003ch2 id=\"running-in-the-cloud\"\u003eRunning in the Cloud\u003c/h2\u003e\u003cp\u003eAlongside the GUI app, the other side of the coin — fully cloud, headless operations — has gotten a lot of work too. Fully static Swift runtime via \u003ccode\u003e\u0026ndash;static-swift-stdlib\u003c/code\u003e, URLSession fixes for musl, and keychain-less credential provisioning from env vars so the same codebase runs cleanly on Ubuntu. One Swift 6 binary, one data model, one API client — running unchanged inside \u003cstrong\u003eAzure Functions, AWS Lambda, or any Linux container\u003c/strong\u003e.\u003c/p\u003e\u003cp\u003eCloud automations that the Linux binary offers:\u003c/p\u003e\u003cul\u003e\u003cli\u003e\u003cstrong\u003eReconciliation (inventory → Apple)\u003c/strong\u003e — a trigger fires off a desired-state list (a CSV, a DB query, an event), the function asks \u003ccode\u003easbmutil\u003c/code\u003e what Apple Business \u0026amp; School Manager currently shows, diffs the two, and issues \u003ccode\u003eassign\u003c/code\u003e/\u003ccode\u003eunassign\u003c/code\u003e calls to close the gap. Anything the inventory system considers authoritative — MDM server, department, ownership tags — can drive the comparison.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eEnrichment (Apple → inventory)\u003c/strong\u003e — a timer pulls the full device list plus AppleCare coverage via \u003ccode\u003easbmutil\u003c/code\u003e, hashes the normalised payload per serial, stores the fingerprint in blob storage, and only writes back to inventory when the Apple-side data has actually changed. Warranty months, coverage status, order number, MDM assignment — all flow in automatically, and the hash cache keeps API traffic and downstream write-load proportional to real change, not fleet size.\u003c/li\u003e\u003c/ul\u003e\u003cp\u003eOutcome: inventory and Apple Business \u0026amp; School Manager stay in lockstep without anyone opening either UI. Warranty data flows in one direction, MDM assignments in the other, both driven by the same Swift binary you'd run on your Mac.\u003c/p\u003e\u003cp\u003eI plan to write in more detail how I'm running these and the code behind them in follow-up posts.\u003c/p\u003e\u003ch2 id=\"how-to-use-all-three\"\u003eHow to Use All Three\u003c/h2\u003e\u003cp\u003eThe CLI has its place. It's the thing I'll use to check which MDM a device is assigned to when I need quick operations — \u003ccode\u003easbmutil list-devices-servers \u0026ndash;mdm \u0026quot;Intune\u0026quot;\u003c/code\u003e piped through \u003ccode\u003ejq\u003c/code\u003e and \u003ccode\u003egrep\u003c/code\u003e, done in a second.\u003c/p\u003e\u003cp\u003eFor bulk operations I reach for the GUI app — load a CSV, select in the table, hit the button. Being able to \u003cem\u003esee\u003c/em\u003e what's assigned, what's unassigned, and peruse the fleet side-by-side catches mistakes before they ship and surfaces clean-up opportunities you'd miss on the command line.\u003c/p\u003e\u003cp\u003eAnd for the continuous, unattended work — nightly syncs, warranty enrichment, inventory reconciliation — the Linux binary inside a serverless function quietly does it in the background, no interaction, automated.\u003c/p\u003e\u003cp\u003eSame data model, same trust store, same API calls underneath.\u003c/p\u003e\u003cfigure class=\"kg-card kg-image-card kg-width-wide\"\u003e\u003cimg src=\"https://github.com/rodchristiansen/asbmutil/raw/main/resources/servers.png?raw=true\" class=\"kg-image\" alt=\"ASBMUtil app\" loading=\"lazy\" width=\"4052\" height=\"2210\"\u003e\u003c/figure\u003e\u003ch2 id=\"grab-it\"\u003eGrab It\u003c/h2\u003e\u003cp\u003eRepo: \u003ca href=\"https://github.com/rodchristiansen/asbmutil\"\u003egithub.com/rodchristiansen/asbmutil\u003c/a\u003e\u003c/p\u003e\u003cp\u003eGrab the latest \u003ccode\u003e.pkg\u003c/code\u003e or \u003ccode\u003e.dmg\u003c/code\u003e from the \u003ca href=\"https://github.com/rodchristiansen/asbmutil/releases/latest\"\u003ereleases page\u003c/a\u003e. The installer drops both the app into \u003ccode\u003e/Applications\u003c/code\u003e and the \u003ccode\u003easbmutil\u003c/code\u003e CLI into \u003ccode\u003e/usr/local/bin\u003c/code\u003e.\u003c/p\u003e\u003cp\u003e\u003cstrong\u003eA note on signing:\u003c/strong\u003e the released artifacts are unsigned and not notarized. On first launch Gatekeeper will complain — clear the quarantine attribute and you're good:\u003c/p\u003e\u003cpre\u003e\u003ccode class=\"language-bash\"\u003exattr -dr com.apple.quarantine /Applications/ASBMUtil.app\nxattr -dr com.apple.quarantine /usr/local/bin/asbmutil\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eOr build and sign it yourself from source — \u003ccode\u003emake build\u003c/code\u003e in the repo will do the whole signing + notarization dance if you drop your own Developer ID into \u003ccode\u003e.env\u003c/code\u003e. As Mac admins you know the drill.\u003c/p\u003e\u003cp\u003eRequirements: macOS 14+, and an AxM API account with a client ID, key ID, and PEM. If you already have the CLI configured, the app reads the same keychain profiles — nothing to migrate.\u003c/p\u003e\u003cp\u003eMore posts coming on the specific things this tool makes trivial that ABM's web UI makes painful — starting with the bulk device-to-server resolver, which I think is the most interesting piece of the codebase. For now: v1 of the app exists, it works, and I'd love to hear where it breaks for you.\u003c/p\u003e\u003c/p\u003e\n","date_published":"2026-04-13T09:29:26Z","id":"https://focused-systems.pages.dev/asbmutil-app-launch/","summary":"The Swift CLI for Apple School \u0026 Business Manager API now has a native SwiftUI app. Same credentials store, same API client, same bulk operations — inside a native app.","tags":["macos","mdm","swift","devops"],"title":"ASBMUtil app","url":"https://focused-systems.pages.dev/asbmutil-app-launch/"},{"authors":[{"name":"Rod Christiansen"}],"content_html":"\u003ch2 id=\"a-native-macos-extension-for-yaml-files\"\u003eA Native macOS Extension for YAML Files\u003c/h2\u003e\u003cp\u003eQuickLook on the Mac doesn\u0026apos;t support previewing the contents of YAML files. You\u0026apos;re digging through repos, config files, CI pipeline files, playbooks, terraform plan and you want to peek at a YAML file without opening it.\u003c/p\u003e\u003cp\u003eYou hit Space.\u003c/p\u003e\u003cp\u003eNothing. Just the generic document icon staring back at you.\u003c/p\u003e\u003cp\u003eQuickLook on the Mac supports text files, plists, JSON\u0026#x2014;but YAML? Nothing out of the box. I fixed that with \u003ca href=\"https://github.com/rodchristiansen/yamlquicklook\"\u003eYAML Quick Look\u003c/a\u003e.\u003c/p\u003e\u003cfigure class=\"kg-card kg-gallery-card kg-width-wide\"\u003e\u003cdiv class=\"kg-gallery-container\"\u003e\u003cdiv class=\"kg-gallery-row\"\u003e\u003cdiv class=\"kg-gallery-image\"\u003e\u003cimg src=\"yamlQuickLook-2.png\" width=\"1024\" height=\"1024\" loading=\"lazy\" alt srcset=\"https://storage.ghost.io/c/7d/ec/7dec1a6c-13d6-4927-b0f1-9d39fe1b152c/content/images/size/w600/2026/03/yamlQuickLook-2.png 600w, https://storage.ghost.io/c/7d/ec/7dec1a6c-13d6-4927-b0f1-9d39fe1b152c/content/images/size/w1000/2026/03/yamlQuickLook-2.png 1000w, https://storage.ghost.io/c/7d/ec/7dec1a6c-13d6-4927-b0f1-9d39fe1b152c/content/images/2026/03/yamlQuickLook-2.png 1024w\" sizes=\"(min-width: 720px) 720px\"\u003e\u003c/div\u003e\u003cdiv class=\"kg-gallery-image\"\u003e\u003cimg src=\"Finder-2026-03-02-11.56.05-1.png\" width=\"1856\" height=\"1922\" loading=\"lazy\" alt srcset=\"https://storage.ghost.io/c/7d/ec/7dec1a6c-13d6-4927-b0f1-9d39fe1b152c/content/images/size/w600/2026/03/Finder-2026-03-02-11.56.05-1.png 600w, https://storage.ghost.io/c/7d/ec/7dec1a6c-13d6-4927-b0f1-9d39fe1b152c/content/images/size/w1000/2026/03/Finder-2026-03-02-11.56.05-1.png 1000w, https://storage.ghost.io/c/7d/ec/7dec1a6c-13d6-4927-b0f1-9d39fe1b152c/content/images/size/w1600/2026/03/Finder-2026-03-02-11.56.05-1.png 1600w, https://storage.ghost.io/c/7d/ec/7dec1a6c-13d6-4927-b0f1-9d39fe1b152c/content/images/2026/03/Finder-2026-03-02-11.56.05-1.png 1856w\" sizes=\"(min-width: 720px) 720px\"\u003e\u003c/div\u003e\u003cdiv class=\"kg-gallery-image\"\u003e\u003cimg src=\"Screenshot-2026-03-02-at-11.56.20-1.png\" width=\"1766\" height=\"1400\" loading=\"lazy\" alt srcset=\"https://storage.ghost.io/c/7d/ec/7dec1a6c-13d6-4927-b0f1-9d39fe1b152c/content/images/size/w600/2026/03/Screenshot-2026-03-02-at-11.56.20-1.png 600w, https://storage.ghost.io/c/7d/ec/7dec1a6c-13d6-4927-b0f1-9d39fe1b152c/content/images/size/w1000/2026/03/Screenshot-2026-03-02-at-11.56.20-1.png 1000w, https://storage.ghost.io/c/7d/ec/7dec1a6c-13d6-4927-b0f1-9d39fe1b152c/content/images/size/w1600/2026/03/Screenshot-2026-03-02-at-11.56.20-1.png 1600w, https://storage.ghost.io/c/7d/ec/7dec1a6c-13d6-4927-b0f1-9d39fe1b152c/content/images/2026/03/Screenshot-2026-03-02-at-11.56.20-1.png 1766w\" sizes=\"(min-width: 720px) 720px\"\u003e\u003c/div\u003e\u003c/div\u003e\u003c/div\u003e\u003c/figure\u003e\u003ch2 id=\"what-it-does\"\u003eWhat It Does\u003c/h2\u003e\u003cp\u003eIt\u0026apos;s a native macOS Quick Look extension that does two things:\u003c/p\u003e\u003col\u003e\u003cli\u003e\u003cstrong\u003ePreview\u003c/strong\u003e: Hit Space on any \u003ccode\u003e.yaml\u003c/code\u003e or \u003ccode\u003e.yml\u003c/code\u003e file, and you get a clean, scrollable plain-text view\u0026#x2014;same experience as \u003ccode\u003e.txt\u003c/code\u003e or \u003ccode\u003e.plist\u003c/code\u003e.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eThumbnails\u003c/strong\u003e: Finder shows file content in the icon itself, so you can tell files apart without opening them.\u003c/li\u003e\u003c/ol\u003e\u003cp\u003eDark mode works. Large files are handled (truncated at 10 MB so it doesn\u0026apos;t eat your RAM). It uses a shared \u003ccode\u003eYAMLFileReader\u003c/code\u003e module backing both extensions, so the behaviour is consistent.\u003c/p\u003e\u003cp\u003eIt\u0026apos;s not a syntax highlighter\u0026#x2014;that was a deliberate choice. Quick Look is for \u003cem\u003eglancing\u003c/em\u003e, not editing. Plain text, monospace, scrollable. That\u0026apos;s it.\u003c/p\u003e\u003ch2 id=\"why-i-wrote-this\"\u003eWhy I Wrote This\u003c/h2\u003e\u003cp\u003eHonestly, it was scratching my own itch. I spend a lot of time in repos and Terraform configs, and the missing YAML preview was a small but consistent annoyance.\u003c/p\u003e\u003cp\u003eThere are third-party alternatives, but most of them are either abandoned, use Homebrew, or are bundled inside larger editor apps. I wanted something clean, native, and deployable via a single pkg.\u003c/p\u003e\u003cp\u003eIt\u0026apos;s available on GitHub under MIT. It requires macOS 14 Sonoma or later.\u003c/p\u003e\u003ch2 id=\"the-app\"\u003eThe App\u003c/h2\u003e\u003cp\u003eThe app is three targets:\u003c/p\u003e\u003cul\u003e\u003cli\u003e\u003ccode\u003eYamlQuickLook\u003c/code\u003e \u0026#x2014; A lightweight container app. You open it once to register the extensions, then mostly ignore it. I added Status, Preview, and Settings tabs to make it feel like a real app rather than a hollow shell.\u003c/li\u003e\u003cli\u003e\u003ccode\u003eYamlQuickLookExtension\u003c/code\u003e \u0026#x2014; The Quick Look preview extension\u003c/li\u003e\u003cli\u003e\u003ccode\u003eYamlQuickLookThumbnailExtension\u003c/code\u003e \u0026#x2014; The thumbnail generator\u003c/li\u003e\u003c/ul\u003e\u003cp\u003eThe shared module (\u003ccode\u003eYamlQuickLookShared\u003c/code\u003e) handles all the file reading logic, so both extensions stay thin. The test suite covers 38 cases across the reader\u0026#x2014;everything from empty files to malformed YAML to files at the size limit.\u003c/p\u003e\u003ch2 id=\"installing-it\"\u003eInstalling It\u003c/h2\u003e\u003cp\u003eThe simplest path: grab the latest zip from \u003ca href=\"https://github.com/rodchristiansen/yamlquicklook/releases\"\u003eReleases\u003c/a\u003e, move the app to \u003ccode\u003e/Applications\u003c/code\u003e, and run the post-install script to strip quarantine and activate the extension:\u003c/p\u003e\u003cpre\u003e\u003ccode class=\"language-bash\"\u003exattr -cr /Applications/YamlQuickLook.app\npluginkit -a /Applications/YamlQuickLook.app/Contents/PlugIns/YamlQuickLookExtension.appex || true\npluginkit -a /Applications/YamlQuickLook.app/Contents/PlugIns/YamlQuickLookThumbnailExtension.appex || true\nqlmanage -r\nqlmanage -r cache\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"building-with-code-signing\"\u003eBuilding with Code Signing\u003c/h2\u003e\u003cp\u003eThe release build isn\u0026apos;t code-signed\u0026#x2014;I don\u0026apos;t have (yet) a Developer ID. You can sign and notarize with your own cert for your environment. I plan to release it onto the Mac App Store.\u003c/p\u003e\u003cp\u003eIf you want a properly signed and notarized build\u0026#x2014;which I\u0026apos;d recommend for org-wide deploys\u0026#x2014;clone the repo and configure signing in Xcode for all three targets:\u003c/p\u003e\u003cpre\u003e\u003ccode class=\"language-bash\"\u003excodebuild -scheme YamlQuickLook \\\n  -configuration Release \\\n  -derivedDataPath build \\\n  CODE_SIGN_IDENTITY=\u0026quot;Developer ID Application: Your Name (TEAM_ID)\u0026quot; \\\n  DEVELOPMENT_TEAM=\u0026quot;TEAM_ID\u0026quot; \\\n  clean build\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eThen notarize it with \u003ccode\u003excrun notarytool\u003c/code\u003e and staple the ticket. The \u003ca href=\"https://github.com/rodchristiansen/yamlquicklook\"\u003eREADME\u003c/a\u003e has the full steps. A signed build skips the \u003ccode\u003exattr\u003c/code\u003e step entirely and removes the quarantine friction for your users.\u003c/p\u003e\u003cp\u003eThe Makefile also has a \u003ccode\u003emake release\u003c/code\u003e target that handles the signing and notarization flow if you\u0026apos;ve set up your credentials in \u003ccode\u003e.env\u003c/code\u003e.\u003c/p\u003e\u003chr\u003e\n","date_published":"2026-03-01T09:00:00Z","id":"https://focused-systems.pages.dev/yamlquicklook/","summary":"QuickLook in the Mac doesn't support YAML files. All you get is the default app icon. I got tired of that and built a Quick Look extension.","tags":["devops","macos","swift","yaml"],"title":"YAML Quick Look","url":"https://focused-systems.pages.dev/yamlquicklook/"},{"authors":[{"name":"Rod Christiansen"}],"content_html":"\u003cblockquote\u003e\u003cstrong\u003eMacDevOps YVR 2025 Companion Post\u003c/strong\u003e\u003c/blockquote\u003e\u003cp\u003eA year ago, we were managing Macs the way most orgs still do: one shared Mac, VNC\u0026#x2019;d into, running a local copy of Munki with no real version control or workflow isolation. Git was an afterthought. Deployments were manual. It worked\u0026#x2014;until it didn\u0026#x2019;t scale.\u003c/p\u003e\u003cp\u003eWe\u0026#x2019;ve since inverted the model. Git is the gate. CI is the deployer. Each admin works from their own machine. And the Munki repo is fully DevOps-native.\u003c/p\u003e\u003cp\u003eHere\u0026#x2019;s how we rebuilt everything using Git, Azure DevOps, Service Bus, pipelines, local caching servers, and inventory automation\u0026#x2014;all open source and cloud-integrated.\u003c/p\u003e\u003cdiv class=\"kg-card kg-file-card\"\u003e\u003ca class=\"kg-file-card-container\" href=\"https://blog.focused.systems/content/files/2025/06/MunkiDevOps-1.pdf\" title=\"Download\" download\u003e\u003cdiv class=\"kg-file-card-contents\"\u003e\u003cdiv class=\"kg-file-card-title\"\u003eMunkiDevOps Presentation Slides\u003c/div\u003e\u003cdiv class=\"kg-file-card-caption\"\u003e\u003c/div\u003e\u003cdiv class=\"kg-file-card-metadata\"\u003e\u003cdiv class=\"kg-file-card-filename\"\u003eMunkiDevOps.pdf\u003c/div\u003e\u003cdiv class=\"kg-file-card-filesize\"\u003e89 MB\u003c/div\u003e\u003c/div\u003e\u003c/div\u003e\u003cdiv class=\"kg-file-card-icon\"\u003e\u003csvg viewbox=\"0 0 24 24\"\u003e\u003cdefs\u003e\u003cstyle\u003e.a{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px;}\u003c/style\u003e\u003c/defs\u003e\u003ctitle\u003edownload-circle\u003c/title\u003e\u003cpolyline class=\"a\" points=\"8.25 14.25 12 18 15.75 14.25\"/\u003e\u003cline class=\"a\" x1=\"12\" y1=\"6.75\" x2=\"12\" y2=\"18\"/\u003e\u003ccircle class=\"a\" cx=\"12\" cy=\"12\" r=\"11.25\"/\u003e\u003c/svg\u003e\u003c/div\u003e\u003c/a\u003e\u003c/div\u003e\u003ch2 id=\"from-manual-to-devops\"\u003eFrom Manual to DevOps\u003c/h2\u003e\u003cp\u003eThe legacy flow was:\u003c/p\u003e\u003cul\u003e\u003cli\u003eGitLab running on-prem\u003c/li\u003e\u003cli\u003eOne shared Mac as the deploy point\u003c/li\u003e\u003cli\u003eOne central repo, updated by many hands\u003c/li\u003e\u003cli\u003eNo pipeline. No hooks. No approval gates.\u003c/li\u003e\u003cli\u003eEveryone stepped on everyone\u0026#x2019;s toes\u003c/li\u003e\u003c/ul\u003e\u003cp\u003eNow we have:\u003c/p\u003e\u003cul\u003e\u003cli\u003eAzure DevOps Git repos and pipelines\u003c/li\u003e\u003cli\u003eGit hooks that upload/download packages automatically\u003c/li\u003e\u003cli\u003eSeparate working copies per admin\u003c/li\u003e\u003cli\u003eLocal caching servers that sync intelligently\u003c/li\u003e\u003cli\u003eA full CI/CD system that integrates with inventory and deploys via pull requests\u003c/li\u003e\u003c/ul\u003e\u003ch2 id=\"architecture-overview\"\u003eArchitecture Overview\u003c/h2\u003e\u003cp\u003eWe\u0026#x2019;ve split this into two core flows:\u003c/p\u003e\u003ch3 id=\"munki-devops-infrastructure\"\u003eMunki DevOps Infrastructure\u003c/h3\u003e\u003cul\u003e\u003cli\u003eAdmins commit to a shared Azure DevOps repo with \u003ccode\u003emanifests/\u003c/code\u003e and \u003ccode\u003epkgsinfo/\u003c/code\u003e\u003c/li\u003e\u003cli\u003eGit hooks (post-commit/merge) run \u003ccode\u003eazcopy sync\u003c/code\u003e to upload or download packages\u003c/li\u003e\u003cli\u003eA pipeline (\u003ccode\u003emunki-push-production.yml\u003c/code\u003e) builds catalogs and updates Azure Storage\u003c/li\u003e\u003cli\u003eLocal caching servers (like PLUTO and PROTEUS) are notified via Azure Service Bus\u003c/li\u003e\u003cli\u003eA daemon listens for commits and runs \u003ccode\u003egit pull\u003c/code\u003e and syncs assets\u003c/li\u003e\u003cli\u003eCDN serves files globally or from on-prem caches\u003c/li\u003e\u003cli\u003eIt\u0026#x2019;s fast, redundant, and observable\u003c/li\u003e\u003c/ul\u003e\u003ch3 id=\"inventory-orchestrated-enrollment\"\u003eInventory-Orchestrated Enrollment\u003c/h3\u003e\u003cul\u003e\u003cli\u003eA polling script checks Snipe-IT for inventory changes and builds new CSVs\u003c/li\u003e\u003cli\u003eCSVs are committed to Git \u0026#x2192; triggers DevOps pipelines \u0026#x2192; runs \u003ccode\u003eenrollment-munki\u003c/code\u003e, \u003ccode\u003eenrollment-intune\u003c/code\u003e, \u003ccode\u003eenrollment-sharepoint\u003c/code\u003e, and more\u003c/li\u003e\u003cli\u003eEach system (Munki, Intune, Fleet, TDX, Papercut) gets updated data\u003c/li\u003e\u003cli\u003ePull requests are the approval gate\u003c/li\u003e\u003cli\u003eEverything is traceable, auditable, and CI-driven\u003c/li\u003e\u003c/ul\u003e\u003ch2 id=\"resources\"\u003eResources\u003c/h2\u003e\u003cul\u003e\u003cli\u003e\u003cstrong\u003ePresentation Repo\u003c/strong\u003e: \u003ca href=\"https://github.com/rodchristiansen/munki-devops\"\u003egithub.com/rodchristiansen/munki-devops\u003c/a\u003e\u003c/li\u003e\u003c/ul\u003e\u003cp\u003eWant to talk shop or ask questions? Connect with me on \u003ca href=\"https://bsky.app/profile/rodchristiansen.net\"\u003eBlueSky\u003c/a\u003e or on the \u003ca href=\"https://github.com/rodchristiansen\"\u003eGitHub\u003c/a\u003e.\u003c/p\u003e\n","date_published":"2025-06-12T17:31:28Z","id":"https://focused-systems.pages.dev/munki-devops/","summary":"How we transformed our Munki workflow into a DevOps-native system using Git, CI/CD pipelines, and Azure. Companion post to my MacDevOps YVR 2025 talk.","tags":["devops","git","ci/cd","munki","azure","macadmin"],"title":"Modern Munki DevOps","url":"https://focused-systems.pages.dev/munki-devops/"},{"authors":[{"name":"Rod Christiansen"}],"content_html":"\u003cp\u003eThe Git ecosystem is phasing out long-lived personal-access tokens. Cloning with Personal Access Tokens are being retired, and new policies now let administrators block or tightly restrict PAT creation. Best-practice docs point to short-lived identity-provider tokens\u0026#x2014;refreshed automatically and governed by conditional-access rules\u0026#x2014;as the preferred way forward.\u003c/p\u003e\u003cp\u003eWiring Git Credential Manager (GCM) into your local global Git helpers you trade fragile over-scoped PATs for one-hour tokens that renew silently and leave no secrets on disk or in build logs.\u003c/p\u003e\u003ch3 id=\"what-this-guide-helps-you-with\"\u003eWhat this guide helps you with\u003c/h3\u003e\u003cul\u003e\u003cli\u003eEnable seamless Single\u0026#x2011;Sign\u0026#x2011;On on \u003cstrong\u003emacOS\u003c/strong\u003e and \u003cstrong\u003eWindows\u003c/strong\u003e for:\u003cul\u003e\u003cli\u003eAzure DevOps (\u003ccode\u003ehttps://dev.azure.com/ORG/\u0026#x2026;\u003c/code\u003e)\u003c/li\u003e\u003cli\u003eGitHub.com or GitHub\u0026#xA0;Enterprise (\u003ccode\u003ehttps://github.com/\u0026#x2026;\u003c/code\u003e)\u003c/li\u003e\u003c/ul\u003e\u003c/li\u003e\u003c/ul\u003e\u003chr\u003e\u003ch2 id=\"macos\"\u003emacOS\u003c/h2\u003e\u003cp\u003eHow to setup GCM for password\u0026#x2011;free access on macOS:\u003c/p\u003e\u003ch3 id=\"prerequisites\"\u003ePrerequisites\u003c/h3\u003e\u003cul\u003e\u003cli\u003eHomebrew\u003c/li\u003e\u003cli\u003eGit \u0026#x2265; the version bundled with Xcode\u0026#x202F;15\u003c/li\u003e\u003cli\u003eGit Credential Manager (installed below)\u003c/li\u003e\u003c/ul\u003e\u003ch3 id=\"install-upgrade-components\"\u003eInstall\u0026#xA0;/\u0026#xA0;upgrade components\u003c/h3\u003e\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ebrew install --cask git-credential-manager\nbrew upgrade git\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"configure-global-helpers\"\u003eConfigure global helpers\u003c/h3\u003e\u003cpre\u003e\u003ccode class=\"language-bash\"\u003egit config --global --replace-all credential.helper manager\ngit config --global --add credential.helper osxkeychain\ngit config --global credential.msauthFlow devicecode\ngit config --global credential.guiPrompt false\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"persist-settings-for-shells-gui-apps\"\u003ePersist settings for shells \u0026amp; GUI apps\u003c/h3\u003e\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eecho \u0026apos;export GCM_MSAUTH_FLOW=devicecode\u0026apos; \u0026gt;\u0026gt; ~/.zprofile\necho \u0026apos;export GCM_GUI_PROMPT=0\u0026apos; \u0026gt;\u0026gt; ~/.zprofile\n\u003cp\u003elaunchctl setenv GCM_MSAUTH_FLOW devicecode\nlaunchctl setenv GCM_GUI_PROMPT 0\n\u003c/code\u003e\u003c/pre\u003e\u003cul\u003e\u003cli\u003e\u003cstrong\u003eVS Code:\u003c/strong\u003e add \u003ccode\u003e\u0026quot;git.terminalAuthentication\u0026quot;: false\u003c/code\u003e to \u003cem\u003esettings.json\u003c/em\u003e\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eGit Tower:\u003c/strong\u003e \u003ccode\u003edefaults write com.fournova.Tower5 UseCredentialManager -bool true\u003c/code\u003e\u003c/li\u003e\u003c/ul\u003e\u003ch3 id=\"first-run\"\u003eFirst run\u003c/h3\u003e\u003cpre\u003e\u003ccode class=\"language-bash\"\u003egit fetch   # single device‑code prompt, then silent\n\u003c/code\u003e\u003c/pre\u003e\u003chr\u003e\u003ch2 id=\"windows\"\u003eWindows\u003c/h2\u003e\u003cp\u003eAnd here's how to setup on Windows, leveraging the Entra ID broker for silent SSO with Azure DevOps and device‑code for GitHub.\u003c/p\u003e\u003ch3 id=\"prerequisites-1\"\u003ePrerequisites\u003c/h3\u003e\u003cul\u003e\u003cli\u003eGit for Windows ≥ 2.45 (bundles GCM v2)\u003c/li\u003e\u003cli\u003eDevice joined to Entra ID (native, hybrid, or AAD‑registered)\u003c/li\u003e\u003c/ul\u003e\u003ch3 id=\"clean-up-old-helpers\"\u003eClean up old helpers\u003c/h3\u003e\u003cpre\u003e\u003ccode class=\"language-powershell\"\u003egit credential-manager unconfigure\ngit credential-manager configure\ngit config \u0026ndash;global \u0026ndash;unset-all credential.helper\ngit config \u0026ndash;global \u0026ndash;remove-section credential\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"enable-broker-sso-azure-devops-and-device-code-github\"\u003eEnable Broker SSO (Azure DevOps) and Device Code (GitHub)\u003c/h3\u003e\u003cpre\u003e\u003ccode class=\"language-powershell\"\u003egit config \u0026ndash;global credential.helper manager-core\ngit config \u0026ndash;global credential.microsoft.sso true\ngit config \u0026ndash;global credential.msauthUseBroker true\ngit config \u0026ndash;global credential.msauthFlow broker\ngit config \u0026ndash;global credential.githubAuthModes devicecode\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"strip-hard%E2%80%91coded-usernames-from-remotes\"\u003eStrip hard‑coded usernames from remotes\u003c/h3\u003e\u003cpre\u003e\u003ccode class=\"language-powershell\"\u003egit remote set-url origin \u003ca href=\"https://dev.azure.com/ORG/PROJECT/_git/REPO\"\u003ehttps://dev.azure.com/ORG/PROJECT/_git/REPO\u003c/a\u003e\ngit remote set-url origin \u003ca href=\"https://github.com/ORG/REPO\"\u003ehttps://github.com/ORG/REPO\u003c/a\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"first-run-1\"\u003eFirst run\u003c/h3\u003e\u003cpre\u003e\u003ccode class=\"language-powershell\"\u003egit fetch   # one Windows dialog, then silent\n\u003c/code\u003e\u003c/pre\u003e\u003chr\u003e\u003ch2 id=\"bulk%E2%80%91fix-existing-repositories-optional\"\u003eBulk‑fix existing repositories (optional)\u003c/h2\u003e\u003cp\u003eReplace the sample paths below with the folder that contains multiple repositories.\u003c/p\u003e\u003cp\u003e\u003cstrong\u003ePowerShell (Windows)\u003c/strong\u003e\u003c/p\u003e\u003cpre\u003e\u003ccode class=\"language-powershell\"\u003eGet-ChildItem C:\\Dev\\Repos -Directory | ForEach-Object {\ngit -C $\u003cem\u003e.FullName remote set-url origin (git -C $\u003c/em\u003e.FullName remote get-url origin -replace '://.\u003cem\u003e@', '://')\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003cstrong\u003ezsh (macOS)\u003c/strong\u003e\u003c/p\u003e\u003cpre\u003e\u003ccode class=\"language-zsh\"\u003efor d in ~/Dev/Repos/\u003c/em\u003e(.); do\nurl=$(git -C \u0026quot;$d\u0026quot; remote get-url origin | sed 's#://.*@#://#')\ngit -C \u0026quot;$d\u0026quot; remote set-url origin \u0026quot;$url\u0026quot;\ndone\n\u003c/code\u003e\u003c/pre\u003e\u003chr\u003e\u003ch2 id=\"troubleshooting\"\u003eTroubleshooting\u003c/h2\u003e\u003cp\u003eRun \u003ccode\u003egit-credential-manager diagnose\u003c/code\u003e for a quick health check. Erase stale tokens with:\u003c/p\u003e\u003cpre\u003e\u003ccode class=\"language-bash\"\u003egit credential-manager erase \u003ca href=\"https://dev.azure.com\"\u003ehttps://dev.azure.com\u003c/a\u003e\ngit credential-manager erase \u003ca href=\"https://github.com\"\u003ehttps://github.com\u003c/a\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eNeed verbose output? Temporarily set:\u003c/p\u003e\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eexport GIT_TRACE=1\nexport GCM_TRACE=1\ngit fetch\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eIf GCM prompts twice on macOS, \u003cem\u003elogin.keychain-db\u003c/em\u003e may be read‑only. Unlock and purge stale entries, then retry:\u003c/p\u003e\u003cpre\u003e\u003ccode class=\"language-bash\"\u003esecurity unlock-keychain ~/Library/Keychains/login.keychain-db\ngit credential-manager erase \u003ca href=\"https://dev.azure.com\"\u003ehttps://dev.azure.com\u003c/a\u003e\ngit credential-manager erase \u003ca href=\"https://github.com\"\u003ehttps://github.com\u003c/a\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003chr\u003e\u003cp\u003eBy switching from long-lived PATs to short-lived tokens through Git Credential Manager, you lock down your supply chain while making everyday Git activity faster and quieter:\u003c/p\u003e\u003cul\u003e\u003cli\u003eMFA is enforced automatically and refreshed in the background.\u003c/li\u003e\u003cli\u003eTokens rotate every hour, slashing the window for theft or replay.\u003c/li\u003e\u003cli\u003eNo secrets leak into scripts, CI logs, or dotfiles—nothing to scrub later.\u003c/li\u003e\u003c/ul\u003e\u003cp\u003eBake these GCM settings into your workstation images and onboarding scripts once, and every clone, fetch, and push runs hands-free from that point on. Stronger security, zero extra clicks, and no browser pop-ups—that’s a win on every front.\u003c/p\u003e\u003c/p\u003e\n","date_published":"2025-06-10T03:53:17Z","id":"https://focused-systems.pages.dev/git-sso-credential-manager/","summary":"End the PAT era—step‑by‑step macOS and Windows guide to enable SSO for Azure DevOps and GitHub using Git Credential Manager.","tags":[],"title":"Passwordless Git SSO with Git Credential Manager","url":"https://focused-systems.pages.dev/git-sso-credential-manager/"},{"authors":[{"name":"Rod Christiansen"}],"content_html":"\u003cp\u003eAlright, let\u0026#x2019;s get meta. This blog is about DevOps and Git\u0026#x2014;so I\u0026#x2019;m kicking it off by showing you how I \u003cem\u003eship\u003c/em\u003e this blog. With \u003ccode\u003eghostpost\u003c/code\u003e, a tool I built that lets me publish to Ghost the same way I manage code: in Git.\u003c/p\u003e\u003ch2 id=\"publishing-like-a-dev-meet-ghostpost\"\u003ePublishing Like a Dev: Meet GhostPost\u003c/h2\u003e\u003cp\u003eYou use \u003ca href=\"https://ghost.org/\"\u003eGhost\u003c/a\u003e because it\u0026#x2019;s modern, open, and built for professional creators. It gives you newsletters, subscriptions, and a full publishing stack that doesn\u0026#x2019;t sell your soul to adtech.\u003c/p\u003e\u003cp\u003eYou use \u003ca href=\"https://git-scm.com/\"\u003eGit\u003c/a\u003e because you want version history, branches, and working with the all mighty plain text.\u003c/p\u003e\u003cp\u003e\u003ccode\u003eghostpost\u003c/code\u003e is a GitOps-style CLI for managing Ghost posts.\u003c/p\u003e\u003cp\u003eEach post lives as a Markdown file in your repo. The front-matter stores all metadata\u0026#x2014;including the Ghost \u003ccode\u003epost_id\u003c/code\u003e.\u003c/p\u003e\u003cp\u003eYou edit locally. You commit. \u003ccode\u003eghostpost\u003c/code\u003e publishes.\u003c/p\u003e\u003ch2 id=\"why-i-built-this\"\u003eWhy I Built This\u003c/h2\u003e\u003cp\u003eI wanted a writing workflow that matched how one might ship code.\u003c/p\u003e\u003cul\u003e\u003cli\u003eNo fragile CMS UI edits\u003c/li\u003e\u003cli\u003eNo \u0026quot;oops I deleted the draft\u0026quot;\u003c/li\u003e\u003cli\u003eNo back-and-forth between browser and source doc\u003c/li\u003e\u003c/ul\u003e\u003cp\u003eThe Ghost GUI becomes just a preview tool. \u003cem\u003eNothing gets edited in it.\u003c/em\u003e\u003c/p\u003e\u003cp\u003eNot necessarily a new idea\u0026#x2014;\u003ca href=\"https://www.how-hard-can-it.be/post2ghost/\"\u003epost2ghost\u003c/a\u003e laid out the same \u0026quot;Articles as Code\u0026quot; concept where content belongs in Git. \u003cstrong\u003eThe CMS should be a rendering layer, not an editing platform.\u003c/strong\u003e\u003c/p\u003e\u003cp\u003eThat post nailed the philosophy:\u003c/p\u003e\u003cul\u003e\u003cli\u003eKeep Markdown files under version control\u003c/li\u003e\u003cli\u003eWrite in your editor of choice\u003c/li\u003e\u003cli\u003eAutomate publishing with API calls\u003c/li\u003e\u003c/ul\u003e\u003cp\u003eIt was still a bit DIY and python based which is a dependency headache -- I love python, but not for cli tools...\u003c/p\u003e\u003cp\u003e\u003ccode\u003eghostpost\u003c/code\u003e takes that same idea and wraps it in a clean CLI. One command and simple.\u003c/p\u003e\u003ch2 id=\"how-it-works\"\u003eHow It Works\u003c/h2\u003e\u003cp\u003eYou write a post in Markdown, with front-matter like this:\u003c/p\u003e\u003cpre\u003e\u003ccode class=\"language-markdown\"\u003e---\ntitle: Your title here\nslug: your-slug\ncustom_excerpt: Short summary here\ntags: [DevOps, Ghost]\nfeature_image: images/cover.jpg\nstatus: draft\n---\n\u003cp\u003eYour content in Markdown.\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eThen you run:\u003c/p\u003e\u003cpre\u003e\u003ccode\u003eghostpost publish -f /path/to/post.md \u0026ndash;editor\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eThe tool takes care of:\u003c/p\u003e\u003cul\u003e\u003cli\u003eCreating the post if it’s new\u003c/li\u003e\u003cli\u003eUpdating the post if it already exists\u003c/li\u003e\u003cli\u003eUploading and rewriting image paths to proper Ghost URLs\u003c/li\u003e\u003c/ul\u003e\u003cp\u003eThere’s no runtime. No daemon. No need to open the CMS.\u003c/p\u003e\u003ch2 id=\"what-you-get\"\u003eWhat You Get\u003c/h2\u003e\u003cul\u003e\u003cli\u003e\u003cstrong\u003eReal version control\u003c/strong\u003e Posts live in Git. You get \u003ccode\u003egit log\u003c/code\u003e, pull requests, inline diffs, CI checks.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eStateless deploys\u003c/strong\u003e Posts can be published from anywhere. Just point to the Markdown file. Or automatically with CI/CD.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eFront-matter is the truth\u003c/strong\u003e Titles, tags, status, authors, descriptions—everything is stored right inside the Markdown file.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eSmart images\u003c/strong\u003e Use local paths. \u003ccode\u003eghostpost\u003c/code\u003e uploads and rewrites them for you.\u003c/li\u003e\u003cli\u003e\u003cstrong\u003eCI-ready\u003c/strong\u003e Validate structure. Block merges on bad metadata. Push on deploy.\u003c/li\u003e\u003c/ul\u003e\u003cp\u003eHere’s a basic example using GitHub Actions:\u003c/p\u003e\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# .github/workflows/publish.yml\njobs:\npublish:\nruns-on: ubuntu-latest\nsteps:\n- uses: actions/checkout@v3\n- run: ghostpost publish -f posts/hello-from-git.md \u0026ndash;editor\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"why-not-just-use-the-ghost-ui\"\u003eWhy Not Just Use the Ghost UI?\u003c/h2\u003e\u003cp\u003eBecause once you’ve versioned your posts, previewed with Markdown, and deployed with a single command—there’s no going back.\u003c/p\u003e\u003cp\u003eNo clunky editors. No missing history. No surprises.\u003c/p\u003e\u003ch2 id=\"get-started\"\u003eGet Started\u003c/h2\u003e\u003cp\u003eCheck out the repo and the readme at \u003ca href=\"https://github.com/rodchristiansen/ghost-gitops-publishing\"\u003e\u003ca href=\"https://github.com/rodchristiansen/ghost-gitops-publishing\"\u003ehttps://github.com/rodchristiansen/ghost-gitops-publishing\u003c/a\u003e\u003c/a\u003e\u003c/p\u003e\u003cp\u003eInstall the binary, connect it to your Ghost API, and start treating your writing like the rest of your infrastructure: reproducible, testable, and versioned.\u003c/p\u003e\u003c/p\u003e\n","date_published":"2025-05-16T03:20:41Z","id":"https://focused-systems.pages.dev/ghost-gitops-publishing/","summary":"A Git-first workflow for publishing to Ghost that mirrors how you ship code versioned, stateless, and CI-friendly.","tags":["gitops","ghost","blogging","devtools"],"title":"GitOps Your Ghost Publishing","url":"https://focused-systems.pages.dev/ghost-gitops-publishing/"},{"authors":[{"name":"Rod Christiansen"}],"content_html":"\u003cp\u003eI\u0026#x2019;ve been managing Macs since before MDM mattered\u0026#x2014;when deployment meant NetBoot imaging, and local software deployment repos with a couple of on-prem Mac minis, login hooks, and ad-hoc ARD commands fired off through a GUI.\u003c/p\u003e\u003cp\u003eIt worked\u0026#x2014;until it didn\u0026apos;t.\u003c/p\u003e\u003cp\u003eWe moved from running local scripts on a shared Mac to each admin working from their own local repo, committing changes through Git, syncing packages with \u003ccode\u003eaz\u003c/code\u003e git hooks. We used to push changes first, then capture them in Git after the fact. Now, Git is the gate. Commits and PRs drive the entire process. Pipelines run automatically, and the cloud becomes the source of truth. Our local caching servers listen for updates\u0026#x2014;pulling down only when there are changes\u0026#x2014;fully inverting the workflow into something distributed, reliable, and scalable. All of it version-controlled. All of it traceable. All of it running on infrastructure provisioned with Terraform.\u003c/p\u003e\u003cp\u003eEvery config, every profile, every software assignment that matters tracked in Git. It\u0026#x2019;s CI/CD deployed, reproducible by design, and logged automatically for traceability.\u003c/p\u003e\u003ch2 id=\"what-this-blog-is-about\"\u003eWhat This Blog Is About\u003c/h2\u003e\u003cp\u003eThis blog is about the journey\u0026#x2014;the migrations, the patterns that emerged, the decisions that held up, and the ones that didn\u0026#x2019;t. And about the new tools and ideas I\u0026apos;ll be building and using along the way.\u003c/p\u003e\u003cp\u003eIf you work in endpoint management with DevOps, you\u0026#x2019;ll find deep dives into:\u003c/p\u003e\u003cul\u003e\u003cli\u003eCI/CD pipelines that manage device tools, states, and configuration\u003c/li\u003e\u003cli\u003eEvery cloud resource under Infrastructure as Code with Terraform\u003c/li\u003e\u003cli\u003eInventory systems that drive deployment logic\u003c/li\u003e\u003cli\u003eAnd Git at the center of it all\u003c/li\u003e\u003c/ul\u003e\u003cp\u003eAs I now manage both macOS and Windows endpoints, I\u0026#x2019;ll be writing about how I\u0026apos;m creating a cohesive, mirrored management system\u0026#x2014;where both platforms are driven by open source tools, DevOps, and Git, and where admins speak the same language on both sides.\u003c/p\u003e\u003cp\u003e\u003cstrong\u003eFocused Systems. Grounded in what actually works and scales. Very opinionated.\u003c/strong\u003e\u003c/p\u003e\u003cp\u003eLet\u0026#x2019;s get to it.\u003c/p\u003e\n","date_published":"2025-05-11T06:36:35Z","id":"https://focused-systems.pages.dev/welcome/","summary":"Exploring modern cloud workflows","tags":["devops","Endpoint Management","ci/cd","macos","Windows"],"title":"Welcome to Focused Systems","url":"https://focused-systems.pages.dev/welcome/"}],"title":"Focused Systems","version":"https://jsonfeed.org/version/1.1"}