-
Notifications
You must be signed in to change notification settings - Fork 0
issue 112 macos primary bundle selection
- Parent issue: #112
- Design PR: #113
- Repository design sources:
doc/issues/issue-022-macos-primary-bundle-selection.md,doc/00-overview.md§6.21,doc/01-manifest-schema.md§5.4.3, anddoc/02-dotnet-architecture.md§9.9–§9.11
The implementation is intentionally split into the following stacked issues. PR numbers are left as
(pending) until the implementation pull requests are created.
| Order | Issue | Feature unit | Branch | Base | PR |
|---|---|---|---|---|---|
| 0 | #112 | Design and acceptance contract | docs/112-macos-primary-bundle-selection |
main |
#113 |
| 1 | #114 | Foundation: manifest selector and Graph mapping | feature/114-macos-primary-bundle-foundation |
docs/112-macos-primary-bundle-selection |
#117 |
| 2 | #115 | Secure PKG inspector and artifact integrity | feature/115-macos-pkg-inspector |
feature/114-macos-primary-bundle-foundation |
#118 |
| 3a | #116 | CLI preflight, --force warning gate |
feature/116-publish-preflight-force |
feature/115-macos-pkg-inspector |
#120 |
| 3b | #116 | CI CLI-version pin, protected forceWarnings, docs |
feature/116-ci-e2e-integration |
feature/116-publish-preflight-force |
#121 |
Each implementation branch is based on the preceding branch. #115 must not be merged before #114,
and #116 must not be merged before #115. The implementation PRs must not silently add types, metadata,
or CLI options that belong to an earlier stack item. #116 itself was split into two stacked PRs rather
than the single feature/116-macos-primary-bundle-operations branch originally sketched above, once its
CLI-side work (preflight/force) and its CI/doc-side work turned out to be independently reviewable units.
PR #120 implements the CLI side in full: PublishPreflight (all-entry, zero-Graph-write artifact
re-verification), --force on package/publish, the TTY [y/N]/non-TTY gate (SemanticWarningGate),
explicit tenant verification before the batch's first Graph call, the additive warningCodes/
forceAcknowledged result-file fields, and the previously-undocumented NoBundlesDetected warning.
789 tests pass (762 existing + 27 new); dotnet build/git diff --check are clean.
PR #121 brings the sample workflows/github-actions/publish-intune-apps.yml and
workflows/azure-pipelines/azure-pipelines.yml in line with this design (pinned CLI version, protected
forceWarnings → production-force environment gate) and updates doc/adr.md/doc/task.md/
doc/05-operation.md/doc/07-local-e2e.md (EN+JA).
Not yet applied to the repository: the credential-free macos-pkg-fixtures job for
.github/workflows/ci.yml, and a new protected .github/workflows/intune-e2e.yml workflow. Both were
authored and handed to the repository owner directly (the implementing session's tool permissions
blocked writes under .github/); applying them, and creating the intune-e2e GitHub Environment with
required reviewers plus a disposable-tenant federated credential, remains outstanding follow-up work
before the protected manual E2E in the "CI and E2E acceptance" section below can actually run.
A macOS PKG (macOSPkgApp/macOSLobApp) can bundle more than one application. Intune uses the first
included application as the detection/reporting primary, so an updater listed first—or a manifest reorder—
can make detection use the wrong application and version.
This design makes primary selection explicit, verifies the downloaded artifact before trusting its metadata, and makes the behavior safe for both interactive and CI publishing. The manifest file itself is never rewritten; only the generated Graph payload is reordered.
Detection.PrimaryBundleId is an optional nullable string that selects the primary entry from
Detection.IncludedApps for both AppType: pkg and AppType: lob.
IncludedApps version fields have the following contract:
-
BundleVersionisCFBundleShortVersionString. FormacOSPkgAppit maps to GraphprimaryBundleVersion; formacOSLobAppit maps tobuildNumberon the selected top-level app and tobuildNumberon everychildAppsentry. -
BundleBuildVersionisCFBundleVersion. It is required forAppType: loband maps to GraphversionNumberon the selected top-level app and on everychildAppsentry. It is optional forAppType: pkgand is not copied into the PKG Graph payload. - The two values are independent. A PKG version must never be copied mechanically into the LOB
versionNumberfield.
Detection:
IgnoreAppVersion: true
PrimaryBundleId: com.microsoft.globalsecureaccess.client
IncludedApps:
- BundleId: com.microsoft.globalsecureaccess.client
BundleVersion: 1.2.3| Manifest value | Behavior |
|---|---|
| Omitted | Existing behavior: IncludedApps[0] is primary and the existing input hash remains unchanged. |
| Set, exactly one match | The matching entry is primary and is moved to the front of the Graph payload. |
| Set, zero or multiple matches | Validation error before any Graph mutation. |
Matching is exact or a segment-boundary prefix: entry.BundleId == PrimaryBundleId, or
entry.BundleId starts with PrimaryBundleId + "." using ordinal, case-sensitive comparison. This
prevents a short, unrelated prefix from selecting the wrong application.
Additional validation rules:
-
IncludedAppscontains between 1 and 500 entries, and BundleIds are ordinal, case-sensitive unique. - Empty or whitespace-only
PrimaryBundleIdvalues fail validation. -
Platform: windowsentries cannot specifyPrimaryBundleId. -
AppType: lobentries require a non-emptyBundleBuildVersionfor every included app. -
IgnoreAppVersionis independent:PrimaryBundleIdselects the application, whileIgnoreAppVersioncontrols Graph version detection. - A bundled updater is excluded by omitting it from
IncludedApps; there is no exclude-list field or built-in updater denylist. Microsoft guidance requiresincludedAppsto contain only applications installed by the PKG.
-
Detection.PrimaryBundleIdis nullable with no default. Canonical manifest JSON omits null properties, so an existing manifest that omits the field retains a byte-identicalmanifestHash/inputHash. -
BundleBuildVersionis also additive. Existing PKG manifests may omit it; LOB manifests must provide it. Setting either field changesinputHashand intentionally causes repackaging/re-upload. -
SchemaVersionremains"1.0"because these are additive fields. -
ManifestLoaderignores unknown properties. Therefore all plan/package/publish jobs and local tooling must use the same pinned CLI version; mixing old and new CLIs can make hashes and payloads disagree.
validate is schema-only. It validates manifest structure, duplicate/count limits, selector semantics,
platform/app-type rules, and repository-relative paths. It does not download a source, calculate a PKG
checksum, or inspect XAR. This feature does not add a filePath source.
package downloads the source, verifies the manifest-declared SHA256, and only then inspects the PKG.
publish does not redownload the source. It re-hashes every artifact, compares it with both the manifest
and package metadata, and re-inspects the same artifact before any Graph mutation.
The inspector reads the XAR header, compressed TOC, and the heap entries referenced by the TOC. It reads
the body of Distribution/PackageInfo XML entries to obtain CFBundleIdentifier,
CFBundleShortVersionString, and, when present, CFBundleVersion. A TOC string search is insufficient.
No cpio payload extraction and no macOS pkgutil dependency are allowed.
The parser is fail-closed with these fixed limits:
- compressed TOC: 16 MiB;
- expanded TOC: 64 MiB;
- one metadata XML entry: 16 MiB;
- bundle records: 4,096;
- XML depth: 64.
It must use checked arithmetic and bounded reads for header fields, offsets, lengths, and heap entries.
Truncation, invalid offsets, unknown compression, invalid UTF-8/XML, missing required metadata,
duplicate bundle IDs, XML depth/size/record limits, DTD or external entities, and cancellation are hard
errors. Hard errors cannot be bypassed with --force.
The inspector records the discovered bundle ID, CFBundleShortVersionString, optional
CFBundleVersion, source entry, selected primary, and diagnostic codes. Semantic warnings are limited to
conditions such as:
- multiple bundles with no explicit
PrimaryBundleId; - a manifest-listed or selected bundle that is absent from the artifact;
- a manifest version mismatch (
BundleVersion, and for LOB alsoBundleBuildVersion).
IgnoreAppVersion changes Graph detection behavior but does not hide an artifact/manifest mismatch from
the inspection report. Semantic warnings are the only conditions that --force can acknowledge.
| Execution context | Behavior |
|---|---|
| Interactive TTY | Aggregate all semantic warnings, show them once, and prompt [y/N]. Declining exits non-zero. |
Non-interactive without --force
|
Fail safely and instruct the operator to use an explicit approved --force. |
--force |
Continue only after hard validation/checksum/inspection/tenant checks pass; record warning codes and force acknowledgement. |
--force never bypasses schema errors, ambiguous selectors, checksum/metadata mismatch, parser limits,
malformed archives, tenant guards, or any other hard error. package may record that warnings were
acknowledged, but publish must not trust that flag without re-hashing and re-inspecting the artifact.
macOS package metadata records an additive metadata schema version, package identity, input hash, content file, content SHA256, exact CLI version, inspector version, discovered bundles, selected primary, warning codes, and whether force was acknowledged. It must not contain source URLs, access tokens, authorization headers, or signed URLs.
For a batch publish, the following preflight is completed for every selected entry before any Graph create, content upload, PATCH, or assignment operation:
- static manifest validation and primary selection;
- artifact existence and safe path validation;
- actual SHA256 compared with the manifest and package metadata;
- bounded XAR re-inspection and comparison with the saved report;
- semantic warning acknowledgement; and
- expected-tenant validation.
If one entry has a hard error or an unacknowledged warning, the entire batch fails with zero Graph mutations. A stale, missing, tampered, or schema-incompatible inspection report is never trusted.
The selected primary is moved to the front without changing manifest order.
| App type | Selected primary mapping | Remaining entries |
|---|---|---|
pkg / macOSPkgApp
|
includedApps[0], primaryBundleId, primaryBundleVersion from BundleId/BundleVersion
|
includedApps preserves the relative order of all non-primary entries |
lob / macOSLobApp
|
childApps[0], top-level bundleId, buildNumber, and versionNumber from BundleId/BundleVersion/BundleBuildVersion
|
Every childApps entry maps its own bundleId, buildNumber, and versionNumber
|
The LOB top-level bundleId is always the selected primary BundleId. The mapping must be verified with a
Graph contract test and a read-back test against the v1.0 resource. PKG uses the beta resource and its
PKG-specific fields.
- All GitHub Actions and Azure Pipelines jobs install/use the same explicitly pinned CLI version.
- Package and inspection metadata are handed between jobs as artifacts; publish never silently downloads a different source.
- Windows and Ubuntu run deterministic XAR fixtures covering exact/prefix/zero/multiple selection,
version mismatch, duplicate records, truncation, invalid XML, limit violations, TTY/non-TTY behavior,
and
--forcescope. - Fake Graph contract tests verify PKG and LOB create/update payloads, including LOB top-level
bundleId,buildNumber,versionNumber, and child app ordering. - A protected manual E2E environment uses an expected tenant and disposable test app/package. It covers create, update, idempotent rerun, warning refusal, approved force, tampered artifact, tenant guard, artifact handoff, and LOB payload read-back.
- Where a managed macOS test device is available, the E2E also waits for install reporting and confirms detection uses the selected primary. Cleanup is scoped to the test package, app, assignments, content, and device-side test state; production resources are never targeted.
- Acceptance is not complete until
dotnet build,dotnet test,git diff --check, and the applicable protected E2E checks pass.
-
doc/00-overview.md§6.21 records the decision and transaction boundary. -
doc/01-manifest-schema.mddocumentsPrimaryBundleId,BundleBuildVersion, limits, mapping, and static/package/publish inspection boundaries. -
doc/02-dotnet-architecture.mddefines the inspector, metadata, Graph mapping, and implementation stack. - The Global Secure Access sample uses an exact primary such as
com.microsoft.globalsecureaccess.client; a broad prefix that matches multiple listed bundles is not a valid sample.
- No built-in known-updater denylist, automatic filtering, or manifest generation.
- No implicit filtering: exclusion is always by omitting an entry from
IncludedApps. - No manifest file reorder; only Graph payload order changes.
- No cpio payload extraction, on-disk verification, or
pkgutildependency. - No
filePathsource is introduced by this feature. - CI does not receive an unconditional
--force; force requires explicit operational approval. - Implementation is delivered through #114, #115, and #116 in the stack above.