-
Notifications
You must be signed in to change notification settings - Fork 1
munki-pkg drop-in compatibility: name handling, keychain paths, --skip-import, receipt-only, notarization deferral #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
jordancalhoun
merged 8 commits into
codecarton:next
from
rodchristiansen:feat/munkipkg-compat
Jul 26, 2026
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
3c5b33e
Reject package names that escape the build directory
rodchristiansen 1ac3570
Expand ${HOME} and tilde in build-info keychain paths
rodchristiansen 85ed93a
Accept --skip-import for munki-pkg compatibility
rodchristiansen 379f1b3
Allow receipt-only projects with no payload and no scripts
rodchristiansen adb06b8
Defer notarization_info validation until notarization runs
rodchristiansen 2ac58fc
Append .pkg to the package name when it lacks the extension
rodchristiansen 620b918
Add hermetic receipt-only build test
rodchristiansen 568e647
Merge next into feat/munkipkg-compat
jordancalhoun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import Foundation | ||
| import Testing | ||
| @testable import SwiftPkgCore | ||
|
|
||
| struct KeychainPathTests { | ||
|
|
||
| @Test("expands ${HOME} to the user's home directory") | ||
| func expandsHome() { | ||
| let expanded = expandKeychainPath("${HOME}/Library/Keychains/signing.keychain") | ||
| #expect(expanded == "\(NSHomeDirectory())/Library/Keychains/signing.keychain") | ||
| #expect(!expanded.contains("${HOME}")) | ||
| } | ||
|
|
||
| @Test("expands a leading tilde") | ||
| func expandsTilde() { | ||
| let expanded = expandKeychainPath("~/Library/Keychains/signing.keychain") | ||
| #expect(expanded == "\(NSHomeDirectory())/Library/Keychains/signing.keychain") | ||
| } | ||
|
|
||
| @Test("leaves an absolute path unchanged") | ||
| func leavesAbsolutePathUnchanged() { | ||
| let path = "/Library/Keychains/System.keychain" | ||
| #expect(expandKeychainPath(path) == path) | ||
| } | ||
|
|
||
| @Test("leaves a bare keychain name unchanged") | ||
| func leavesBareNameUnchanged() { | ||
| #expect(expandKeychainPath("login.keychain") == "login.keychain") | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import Foundation | ||
| import Testing | ||
| @testable import SwiftPkgCore | ||
|
|
||
| struct NotarizationDeferTests { | ||
| private var defaults: PackageConfiguration { .defaults(for: URL(fileURLWithPath: "/tmp/Proj")) } | ||
|
|
||
| @Test("incomplete notarization_info loads as .invalid instead of throwing") | ||
| func incompleteLoadsAsInvalid() throws { | ||
| let values: [String: Any] = [ | ||
| "name": "Proj.pkg", "identifier": "com.example.proj", "version": "1.0", | ||
| "notarization_info": ["password": "abcd-efgh-ijkl-mnop"] | ||
| ] | ||
| let config = try PackageConfiguration(values: values, defaults: defaults) | ||
| guard case .invalid = try #require(config.notarization?.authentication) else { | ||
| Issue.record("Expected .invalid authentication") | ||
| return | ||
| } | ||
| } | ||
|
|
||
| @Test("complete notarization_info still parses to a usable authentication") | ||
| func completeParses() throws { | ||
| let profileValues: [String: Any] = [ | ||
| "name": "Proj.pkg", "identifier": "com.example.proj", "version": "1.0", | ||
| "notarization_info": ["keychain_profile": "notary"] | ||
| ] | ||
| let profile = try PackageConfiguration(values: profileValues, defaults: defaults) | ||
| guard case .keychainProfile("notary") = try #require(profile.notarization?.authentication) else { | ||
| Issue.record("Expected .keychainProfile") | ||
| return | ||
| } | ||
|
|
||
| let appleValues: [String: Any] = [ | ||
| "name": "Proj.pkg", "identifier": "com.example.proj", "version": "1.0", | ||
| "notarization_info": ["apple_id": "a@b.com", "team_id": "TEAM"] | ||
| ] | ||
| let apple = try PackageConfiguration(values: appleValues, defaults: defaults) | ||
| guard case .appleID = try #require(apple.notarization?.authentication) else { | ||
| Issue.record("Expected .appleID") | ||
| return | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import Foundation | ||
| import Testing | ||
| @testable import SwiftPkgCore | ||
|
|
||
| struct PackageNameExtensionTests { | ||
| private var defaults: PackageConfiguration { .defaults(for: URL(fileURLWithPath: "/tmp/Proj")) } | ||
|
|
||
| private func config(name: String, version: String = "1.0") throws -> PackageConfiguration { | ||
| try PackageConfiguration( | ||
| values: ["name": name, "identifier": "com.example.proj", "version": version], | ||
| defaults: defaults | ||
| ) | ||
| } | ||
|
|
||
| @Test("a name without .pkg gains the extension") | ||
| func appendsExtension() throws { | ||
| let resolved = try config(name: "MunkiBootstrap").substitutingVersion() | ||
| #expect(resolved.name == "MunkiBootstrap.pkg") | ||
| } | ||
|
|
||
| @Test("a name already ending in .pkg is left unchanged") | ||
| func keepsExtension() throws { | ||
| let resolved = try config(name: "AdminDock.pkg").substitutingVersion() | ||
| #expect(resolved.name == "AdminDock.pkg") | ||
| } | ||
|
|
||
| @Test("the extension is appended after ${version} substitution") | ||
| func appendsAfterVersionSubstitution() throws { | ||
| let resolved = try config(name: "Tool-${version}", version: "2.3").substitutingVersion() | ||
| #expect(resolved.name == "Tool-2.3.pkg") | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import Foundation | ||
| import Testing | ||
| @testable import SwiftPkgCore | ||
|
|
||
| struct PackageNameValidationTests { | ||
|
|
||
| @Test("rejects names that escape the build directory", arguments: [ | ||
| "../evil.pkg", | ||
| "../../tmp/evil.pkg", | ||
| "sub/dir/evil.pkg", | ||
| "/etc/evil.pkg", | ||
| "..", | ||
| ".", | ||
| "", | ||
| ]) | ||
| func rejectsUnsafeNames(_ name: String) { | ||
| #expect(throws: MunkiPkgError.self) { | ||
| try PackageBuildCoordinator.validatePackageName(name) | ||
| } | ||
| } | ||
|
|
||
| @Test("accepts safe single-component names", arguments: [ | ||
| "MyApp-1.0.pkg", | ||
| "com.example.thing.pkg", | ||
| "package_2026.07.18.pkg", | ||
| "no-extension", | ||
| ]) | ||
| func acceptsSafeNames(_ name: String) throws { | ||
| try PackageBuildCoordinator.validatePackageName(name) | ||
| } | ||
|
|
||
| // End-to-end: a malicious build-info name must fail before pkgbuild runs. | ||
| @Test("build with a traversal name throws and never invokes pkgbuild") | ||
| func buildRejectsTraversalNameBeforeRunning() async throws { | ||
| let temp = try TemporaryDirectory() | ||
| defer { temp.remove() } | ||
| let project = temp.url.appendingPathComponent("Evil", isDirectory: true) | ||
| let payload = project.appendingPathComponent("payload", isDirectory: true) | ||
| try FileManager.default.createDirectory(at: payload, withIntermediateDirectories: true) | ||
| try write("payload", to: payload.appendingPathComponent("file.txt")) | ||
| try write( | ||
| #"{"name":"../evil.pkg","identifier":"com.test.evil","version":"1.0"}"#, | ||
| to: project.appendingPathComponent("build-info.json") | ||
| ) | ||
|
|
||
| let runner = RecordingRunner() | ||
| let coordinator = PackageBuildCoordinator(fileManager: .default, runner: runner, console: makeConsole()) | ||
| await #expect(throws: MunkiPkgError.self) { | ||
| try await coordinator.buildPackage(in: project, configuration: PackageBuildOptions()) | ||
| } | ||
| #expect(runner.calls.isEmpty) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import Foundation | ||
| import Testing | ||
| @testable import SwiftPkgCore | ||
|
|
||
| struct ReceiptOnlyBuildTests { | ||
|
|
||
| // A project with neither payload nor scripts is valid: it builds a | ||
| // receipt-only package. pkgbuild must be invoked with --nopayload (and no | ||
| // --root), matching munki-pkg — otherwise the build would fail looking for | ||
| // a payload that isn't there. | ||
| @Test("receipt-only project builds with pkgbuild --nopayload") | ||
| func receiptOnlyUsesNopayload() async throws { | ||
| let temp = try TemporaryDirectory() | ||
| defer { temp.remove() } | ||
| let project = temp.url.appendingPathComponent("ReceiptOnly", isDirectory: true) | ||
| try FileManager.default.createDirectory(at: project, withIntermediateDirectories: true) | ||
| try write( | ||
| #"{"name":"ReceiptOnly-1.0.pkg","identifier":"com.test.receipt","version":"1.0"}"#, | ||
| to: project.appendingPathComponent("build-info.json") | ||
| ) | ||
|
|
||
| let runner = RecordingRunner() | ||
| let coordinator = PackageBuildCoordinator(fileManager: .default, runner: runner, console: makeConsole()) | ||
| try await coordinator.buildPackage(in: project, configuration: PackageBuildOptions(skipsSigning: true)) | ||
|
|
||
| let pkgbuild = try #require(runner.calls.first { $0.executable.hasSuffix("pkgbuild") }) | ||
| #expect(pkgbuild.arguments.contains("--nopayload")) | ||
| #expect(!pkgbuild.arguments.contains("--root")) | ||
| } | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Add hermetic receipt-only compatibility coverage.
Add a Swift test that builds a project with neither
payloadnorscriptsusingTemporaryDirectoryandRecordingRunner, and asserts the generated command uses the receipt-only path.As per coding guidelines, behavior changes affecting “payload-free packages” require targeted compatibility tests, and unit tests must use
TemporaryDirectoryandRecordingRunner.🤖 Prompt for AI Agents
Source: Coding guidelines