Skip to content

issue 112 macos primary bundle selection

Kazushi Kamegawa edited this page Aug 26, 2026 · 4 revisions

Issue #112: macOS PKG detection primary bundle selection

日本語

Tracking

  • 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, and doc/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.

#116 implementation status (2026-08-26)

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.

Goal

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.

Manifest contract

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:

  • BundleVersion is CFBundleShortVersionString. For macOSPkgApp it maps to Graph primaryBundleVersion; for macOSLobApp it maps to buildNumber on the selected top-level app and to buildNumber on every childApps entry.
  • BundleBuildVersion is CFBundleVersion. It is required for AppType: lob and maps to Graph versionNumber on the selected top-level app and on every childApps entry. It is optional for AppType: pkg and is not copied into the PKG Graph payload.
  • The two values are independent. A PKG version must never be copied mechanically into the LOB versionNumber field.
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:

  • IncludedApps contains between 1 and 500 entries, and BundleIds are ordinal, case-sensitive unique.
  • Empty or whitespace-only PrimaryBundleId values fail validation.
  • Platform: windows entries cannot specify PrimaryBundleId.
  • AppType: lob entries require a non-empty BundleBuildVersion for every included app.
  • IgnoreAppVersion is independent: PrimaryBundleId selects the application, while IgnoreAppVersion controls 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 requires includedApps to contain only applications installed by the PKG.

Hash compatibility and CLI versioning

  • Detection.PrimaryBundleId is nullable with no default. Canonical manifest JSON omits null properties, so an existing manifest that omits the field retains a byte-identical manifestHash/inputHash.
  • BundleBuildVersion is also additive. Existing PKG manifests may omit it; LOB manifests must provide it. Setting either field changes inputHash and intentionally causes repackaging/re-upload.
  • SchemaVersion remains "1.0" because these are additive fields.
  • ManifestLoader ignores 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.

Static validation and PKG inspection boundaries

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.

Bounded XAR metadata inspection

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.

Inspection diagnostics and confirmation

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 also BundleBuildVersion).

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.

Artifact metadata and publish preflight

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:

  1. static manifest validation and primary selection;
  2. artifact existence and safe path validation;
  3. actual SHA256 compared with the manifest and package metadata;
  4. bounded XAR re-inspection and comparison with the saved report;
  5. semantic warning acknowledgement; and
  6. 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.

Graph payload mapping

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.

CI and E2E acceptance

  • 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 --force scope.
  • 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.

Documentation and sample updates

  • doc/00-overview.md §6.21 records the decision and transaction boundary.
  • doc/01-manifest-schema.md documents PrimaryBundleId, BundleBuildVersion, limits, mapping, and static/package/publish inspection boundaries.
  • doc/02-dotnet-architecture.md defines 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.

Scope boundaries

  • 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 pkgutil dependency.
  • No filePath source 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.

Clone this wiki locally