A team can archive an app successfully after upgrading dependencies, only to discover just before release that a new component is missing from the open-source license inventory. The problem is rarely the build itself. Dependencies come from several places: SwiftPM has its own lockfile, CocoaPods uses a separate directory tree, and the repository may also contain copied source code or bundled binaries. Running the check in cloud Mac CI lets the same project generate an inventory, evaluate it against policy, and retain evidence in a controlled workspace, instead of forcing the release owner to search through directories at the last minute.
Define What the Gate Must Answer
An enforceable gate must answer at least four questions: which components the build actually resolved, which versions or commits it used, where the full license texts are, and whether those terms have already been approved. Do not approve a release based on a filename search alone. The presence of a LICENSE file does not mean its contents are unchanged, nor does it prove that the current target actually links the component.
Keep the machine-readable inventory separate from the human-readable report. Use JSON for the inventory, recording each component’s name, source, locked version, license type, text hash, and discovery path. Use Markdown for the report, showing additions, removals, and changes. Retain both as pipeline artifacts.
The gate verifies that the observed dependency facts match the established policy. When it finds an unknown item, it should stop the release and request review rather than infer a course of action from the license name.
Inventory the Dependencies Used by the Build
First let Xcode resolve the dependencies, then inspect the directories actually used by the workspace. This prevents stale caches on a developer machine from entering the scan. Give every job its own DerivedData directory and clean it up after the job finishes.
set -euo pipefail
ROOT="$(pwd)"
OUT="$ROOT/build/license-audit"
DD="$ROOT/build/DerivedData"
mkdir -p "$OUT"
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-resolvePackageDependencies \
-derivedDataPath "$DD"
find "$DD/SourcePackages/checkouts" \
-maxdepth 3 \
-type f \
\( -iname 'license*' -o -iname 'copying*' -o -iname 'notice*' \) \
-print0 | sort -z > "$OUT/swiftpm-license-files.bin"
find "$ROOT/Pods" \
-maxdepth 4 \
-type f \
\( -iname 'license*' -o -iname 'copying*' -o -iname 'notice*' \) \
-print0 | sort -z > "$OUT/pods-license-files.bin"
File extensions alone are not enough to identify binary dependencies. Also record the internal component identifier associated with the download URL, its checksum and version, and any notices distributed with the package. For source copied into the repository, maintain a compliance/manual-components.json file that clearly assigns an owner to each directory, so every scan does not have to rely on fuzzy matching.
Record Every Lockfile as Well
Copy Package.resolved, Podfile.lock, and the manual component inventory into the report directory. The gate must verify that every component in the inventory can be traced to a locked source. It must also perform the reverse check: direct dependencies in the lockfiles must not disappear from the report. If the project contains multiple workspaces, resolve each one separately rather than checking only the main application.
Normalize License Texts and Compute Fingerprints
License texts often differ only in line endings or trailing spaces. Before computing SHA-256, normalize them to UTF-8 with LF line endings and remove trailing whitespace. Do not remove body paragraphs, copyright lines, or years, because doing so could hide real changes.
python3 - <<'PY'
from pathlib import Path
import hashlib
import json
root = Path("build/license-audit/licenses")
rows = []
for path in sorted(root.rglob("*")):
if not path.is_file():
continue
text = path.read_text(encoding="utf-8", errors="strict")
normalized = "\n".join(line.rstrip() for line in text.splitlines()).strip() + "\n"
digest = hashlib.sha256(normalized.encode("utf-8")).hexdigest()
rows.append({"path": str(path), "sha256": digest})
Path("build/license-audit/license-hashes.json").write_text(
json.dumps(rows, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8"
)
PY
Store the detected license type separately from the text fingerprint. The type is used for policy matching, while the fingerprint reveals quiet upstream changes. If the license cannot be identified reliably, mark it as UNKNOWN. Never classify it automatically just because the filename looks familiar.
Control Changes with Policy as Code
Keep the policy file under version control and modify it through a clearly defined code review process. License identifiers can be assigned allow, review, or deny states, while approved text hashes can be recorded for specific components. Evaluate results using the following rules:
| Condition | CI result | Next action |
|---|---|---|
| Known component with unchanged version and text | Pass | Preserve the report |
| New version with an unchanged license fingerprint | Review | Confirm the component changes |
| New component or unknown license | Block | Add provenance and approval |
| License text changed for an existing component | Block | Compare the full-text diff |
| Component exists in a lockfile but is missing from the report | Block | Fix the discovery rules |
An allowlist should not contain only broad license types. A component may include an additional NOTICE, dual licensing, or separate terms for bundled resources, so the report must retain every relevant file. For binary packages, also verify that the checksum of the current download matches the approved record.
Wire the Report into Merge and Release Pipelines
Routine merge requests can resolve dependencies, check differences, and generate the report. Release jobs should also inspect the archive contents to confirm that the final artifact corresponds to the same lockfile. Store the baseline in compliance/license-baseline.json, and have CI compare structured fields rather than colorized terminal output.
When the gate fails, the log should identify the component, old value, new value, and evidence path directly instead of returning only “license check failed.” At a minimum, the report directory should include copies of the lockfiles, normalized texts, the hash inventory, the policy version, and a summary of differences. Concurrent jobs on ArmCLI should each use an isolated working directory so a dependency update in one job cannot contaminate another job’s scan.
Pre-Release Checklist
Before release, confirm that dependency resolution did not use any unrecorded local paths; every direct and transitive dependency has a version and source; the unknown-license count is zero; directories listed in the manual component inventory still exist; the report and archive use the same commit; and CI artifacts contain no access tokens, private download URLs, or unredacted environment variables.
The value of this process goes beyond generating a page of notices. It turns dependency changes into reviewable engineering events. As long as lockfiles, license fingerprints, and the policy baseline remain synchronized, the team can detect issues when a dependency enters the release branch instead of tracing its origin after the archive is complete.
Frequently asked questions
Is checking Package.resolved enough?
No. It records resolved SwiftPM versions, but it does not prove that license texts were found and does not cover CocoaPods, copied source code, or binary dependencies.
Which changes should block a release?
Block a new dependency with no license file, a license outside the approved policy, a mismatch with lockfiles, or any unexpected change to an existing license text.
Prepare a dedicated physical Mac for your next build
Choose the model, data center, and rental term for your task. Nodes run normally year-round; the live availability status is shown in the console.