Skip to content

Add --provenance attestation sidecar - #25

Merged
jordancalhoun merged 3 commits into
codecarton:nextfrom
rodchristiansen:feat/provenance
Aug 2, 2026
Merged

Add --provenance attestation sidecar#25
jordancalhoun merged 3 commits into
codecarton:nextfrom
rodchristiansen:feat/provenance

Conversation

@rodchristiansen

@rodchristiansen rodchristiansen commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

--provenance writes a sidecar attestation describing how the package was built (inputs, tool version, resulting digest) for supply-chain traceability.

Tests: ProvenanceTests; verify-loop.sh covers it; full suite green.


Part of a 9-PR series splitting a batch of features into small, themed, independently reviewable PRs. Each applies cleanly to main on its own; the ordering below only minimizes rebases as they land:

  1. Process and exit-code correctness: pipe drain, notarization failure, distinct exit codes #19 — process & exit-code correctness
  2. Version resolution: --pkg-version/--output-dir overrides and dynamic version tokens #20 — version resolution · munki-pkg drop-in compatibility: name handling, keychain paths, --skip-import, receipt-only, notarization deferral #21 — munki-pkg compatibility
  3. Add --output-format json build manifest #22 — json manifest · Add --lint to validate a project without building #23 — lint · Add --verify to check the built package against build-info #24 — verify · Add --provenance attestation sidecar #25 — provenance · Add .env build-time variable substitution for scripts #26 — .env substitution
  4. Add GitHub Action and Azure DevOps templates for building packages #27 — CI templates

Happy to squash, split, or reorder any of these to suit your review preferences.

Summary by CodeRabbit

  • New Features
    • Added an optional --provenance flag to generate a .provenance.json sidecar next to built packages.
    • Provenance records build/package details, integrity hashes, a deterministic input_digest, and optional source control commit/remote info, with remote URL credential sanitization.
  • Tests
    • Added deterministic provenance and digest verification, including JSON round-tripping and input_digest change detection when payload contents, executable bits, or symlink targets change.
  • Chores
    • Updated the verification workflow to generate and validate provenance output.
    • Updated CLI help/usage to document --provenance.

--provenance writes <pkg>.provenance.json next to the package recording the
tool version, build time, name/version/identifier, package path and sha256,
a deterministic input digest (sorted hash of payload + scripts + build-info),
and the git commit/remote when the project is a repo. Remote URLs are
stripped of user:pass@ credentials before recording. Threaded through
PackageBuildOptions so the GUI can adopt it.

git is added to ToolPaths and probed via ProcessRunning (absent repo -> null).

Tests: ProvenanceTests (credential stripping incl. scp-style passthrough;
git capture; deterministic digest; digest changes with inputs; snake_case
JSON round-trip). verify-loop.sh builds with --provenance and validates the
sidecar keys and that sha256 matches the real package.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3b663473-ac72-46d7-a913-0d437fc4fc55

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

Package provenance

Layer / File(s) Summary
Provenance model and metadata computation
swiftpkg/Provenance.swift, swiftpkg/Support.swift
Adds codable provenance metadata, deterministic package and input hashing, sanitized Git metadata, and a Git tool path.
CLI and package build integration
swiftpkg/PackageBuildOptions.swift, swiftpkg/PackageBuilder.swift, swiftpkgCLI/CLI.swift
Adds the --provenance option, forwards it to package builds, and writes an atomic provenance sidecar after packaging.
Provenance tests and verification
swiftpkgTests/ProvenanceTests.swift, scripts/verify-loop.sh
Tests serialization, Git handling, digest changes, and validates generated provenance metadata and package hashes end to end.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant PackageBuildCoordinator
  participant ProvenanceBuilder
  participant PackageFile
  participant ProvenanceJSON
  CLI->>PackageBuildCoordinator: enable provenance
  PackageBuildCoordinator->>PackageFile: build package
  PackageBuildCoordinator->>ProvenanceBuilder: build provenance
  ProvenanceBuilder->>PackageFile: compute SHA-256
  ProvenanceBuilder->>ProvenanceJSON: serialize metadata
  PackageBuildCoordinator->>ProvenanceJSON: atomically write sidecar
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a --provenance attestation sidecar.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@rodchristiansen

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@rodchristiansen

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 57 minutes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
swiftpkgTests/ProvenanceTests.swift (1)

28-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test for payload-free provenance generation.

Both tests populate payload/, so the inputDigest code path that skips a missing payload/scripts directory (Provenance.swift lines 69-78) is untested here. As per coding guidelines, "payload-free packages" is explicitly called out as a scenario requiring a targeted compatibility test when new behavior touches it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@swiftpkgTests/ProvenanceTests.swift` around lines 28 - 86, Add a targeted
test for payload-free provenance generation alongside buildsProvenance and
digestChangesWithInputs, creating a project with build metadata and output but
no payload or scripts directory. Build provenance through ProvenanceBuilder and
assert it succeeds with a valid deterministic inputDigest, covering the
missing-directory handling in the provenance input collection path.

Source: Coding guidelines

swiftpkg/Provenance.swift (2)

67-92: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

inputDigest loads each file fully into memory; provenanceSHA256 streams in chunks.

Two hashing helpers in the same file use different strategies: inputDigest reads each file entirely via Data(contentsOf:) per iteration, while provenanceSHA256(ofFileAt:) streams in 1 MB chunks via FileHandle. For large payload files this creates avoidable memory spikes and duplicated hex-encoding logic (hasher.finalize().map { String(format: "%02x", $0) }.joined() appears twice). Consider reusing the streaming approach/helper for both.

♻️ Suggested consolidation
-        for entry in entries {
-            hasher.update(data: Data(entry.path.utf8))
-            hasher.update(data: Data([0]))
-            hasher.update(data: try Data(contentsOf: entry.url))
-        }
-        return hasher.finalize().map { String(format: "%02x", $0) }.joined()
+        for entry in entries {
+            hasher.update(data: Data(entry.path.utf8))
+            hasher.update(data: Data([0]))
+            try streamHash(of: entry.url, into: &hasher)
+        }
+        return hexDigest(hasher.finalize())

Add a shared streamHash(of:into:) helper (chunked FileHandle reads) and a shared hexDigest(_:) helper, and reuse both from provenanceSHA256(ofFileAt:).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@swiftpkg/Provenance.swift` around lines 67 - 92, Update inputDigest to hash
each entry through a shared chunked FileHandle streaming helper instead of
Data(contentsOf:), preserving the existing path-and-null-byte prefix ordering.
Extract the repeated SHA256 hex conversion into a shared hexDigest helper, and
reuse both helpers from inputDigest and provenanceSHA256(ofFileAt:).

30-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Static analysis: prefer failable String(bytes:encoding:).

SwiftLint flags String(decoding: try encoder.encode(self), as: UTF8.self). Functionally fine here since JSONEncoder output is guaranteed valid UTF-8, so this is a low-value stylistic nit, but addressing it would keep lint clean.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@swiftpkg/Provenance.swift` around lines 30 - 34, Update jsonString() to
construct the encoded JSON using failable String(bytes:encoding:) instead of
String(decoding:as:), while preserving the existing JSONEncoder configuration
and throwing behavior for encoding failures. Handle the optional string result
appropriately so the method still returns a String.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@swiftpkg/Provenance.swift`:
- Around line 67-92: Update inputDigest to hash each entry through a shared
chunked FileHandle streaming helper instead of Data(contentsOf:), preserving the
existing path-and-null-byte prefix ordering. Extract the repeated SHA256 hex
conversion into a shared hexDigest helper, and reuse both helpers from
inputDigest and provenanceSHA256(ofFileAt:).
- Around line 30-34: Update jsonString() to construct the encoded JSON using
failable String(bytes:encoding:) instead of String(decoding:as:), while
preserving the existing JSONEncoder configuration and throwing behavior for
encoding failures. Handle the optional string result appropriately so the method
still returns a String.

In `@swiftpkgTests/ProvenanceTests.swift`:
- Around line 28-86: Add a targeted test for payload-free provenance generation
alongside buildsProvenance and digestChangesWithInputs, creating a project with
build metadata and output but no payload or scripts directory. Build provenance
through ProvenanceBuilder and assert it succeeds with a valid deterministic
inputDigest, covering the missing-directory handling in the provenance input
collection path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2d35bbd2-885e-453b-898e-bdbc66b5ff2a

📥 Commits

Reviewing files that changed from the base of the PR and between 25f222d and 83838ec.

📒 Files selected for processing (7)
  • scripts/verify-loop.sh
  • swiftpkg/PackageBuildOptions.swift
  • swiftpkg/PackageBuilder.swift
  • swiftpkg/Provenance.swift
  • swiftpkg/Support.swift
  • swiftpkgCLI/CLI.swift
  • swiftpkgTests/ProvenanceTests.swift

@rodchristiansen

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
swiftpkgTests/ProvenanceTests.swift (1)

5-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer the shared RecordingRunner over a bespoke GitRunner test double.

As per path instructions, swiftpkgTests/**/*.swift: "Keep unit tests hermetic: use TemporaryDirectory and RecordingRunner from TestSupport.swift instead of real subprocesses when testing command construction." This file defines its own ProcessRunning stub instead of reusing RecordingRunner, duplicating existing test infrastructure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@swiftpkgTests/ProvenanceTests.swift` around lines 5 - 16, Replace the bespoke
GitRunner test double with the shared RecordingRunner from TestSupport.swift in
the affected provenance tests. Configure its recorded command outputs to provide
the commit and remote values currently returned by GitRunner, while preserving
the tests’ existing command-construction assertions and hermetic behavior.

Source: Path instructions

scripts/verify-loop.sh (1)

74-91: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider also exercising --provenance against a payload-free/empty-payload project.

As per path instructions, scripts/verify-loop.sh must exercise "payload-free and empty-payload projects" among other scenarios. The new provenance block only tests a project with a payload; adding a quick payload-free variant would confirm inputDigest behaves correctly when the "payload"/"scripts" directories don't exist.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/verify-loop.sh` around lines 74 - 91, Extend the provenance coverage
in the verification flow to include a payload-free or empty-payload project
where neither payload nor scripts directories exist. Invoke the existing
--provenance path for that project and validate the generated provenance,
especially that input_digest is produced correctly without payload content,
while preserving the current payload-backed assertions.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@swiftpkg/Provenance.swift`:
- Around line 65-98: Update inputDigest(for:) to include symlink entries instead
of filtering them out, hashing a deterministic representation of each link
target alongside its relative path. Also read and include POSIX permission bits
for every included filesystem entry, especially scripts, in the digest input.
Preserve sorted project-relative ordering and the existing content hashing for
regular files.

---

Nitpick comments:
In `@scripts/verify-loop.sh`:
- Around line 74-91: Extend the provenance coverage in the verification flow to
include a payload-free or empty-payload project where neither payload nor
scripts directories exist. Invoke the existing --provenance path for that
project and validate the generated provenance, especially that input_digest is
produced correctly without payload content, while preserving the current
payload-backed assertions.

In `@swiftpkgTests/ProvenanceTests.swift`:
- Around line 5-16: Replace the bespoke GitRunner test double with the shared
RecordingRunner from TestSupport.swift in the affected provenance tests.
Configure its recorded command outputs to provide the commit and remote values
currently returned by GitRunner, while preserving the tests’ existing
command-construction assertions and hermetic behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 95ada447-5d5b-4b5b-b2a5-9ce6a504cb17

📥 Commits

Reviewing files that changed from the base of the PR and between 25f222d and 83838ec.

📒 Files selected for processing (7)
  • scripts/verify-loop.sh
  • swiftpkg/PackageBuildOptions.swift
  • swiftpkg/PackageBuilder.swift
  • swiftpkg/Provenance.swift
  • swiftpkg/Support.swift
  • swiftpkgCLI/CLI.swift
  • swiftpkgTests/ProvenanceTests.swift

Comment thread swiftpkg/Provenance.swift
The input digest skipped symlinks entirely and never hashed POSIX permission
bits, so a changed framework symlink target or a toggled executable bit produced
an identical input_digest — defeating the attestation. Symlinks now contribute
their destination and every entry contributes its mode.
@rodchristiansen

Copy link
Copy Markdown
Contributor Author

Addressed in the latest commit. The enumerator now includes symlinks (hashing their destination rather than skipping them), and every entry's POSIX mode bits are folded into the digest, so a toggled executable bit or a re-pointed Versions/Current link now changes input_digest. Added tests for both. Note: I hash the mode as two bytes rather than one — the suggested UInt8(mode & 0o777) traps at runtime since 0o777 (511) exceeds UInt8. Suite green.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
swiftpkgTests/ProvenanceTests.swift (1)

51-53: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert digest values, not only their lengths.

count == 64 allows any 64-character string, including an incorrect or non-hex digest, to pass. Compare the package digest against an independently computed expected SHA-256 for PKGDATA, and validate the input digest against a deterministic golden value or independent reference implementation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@swiftpkgTests/ProvenanceTests.swift` around lines 51 - 53, Update the
assertions in ProvenanceTests around provenance to compare sha256 and
inputDigest against independently computed deterministic SHA-256 golden values
for PKGDATA, rather than checking only that each string has length 64. Preserve
the existing provenance identifier assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@swiftpkgTests/ProvenanceTests.swift`:
- Around line 51-53: Update the assertions in ProvenanceTests around provenance
to compare sha256 and inputDigest against independently computed deterministic
SHA-256 golden values for PKGDATA, rather than checking only that each string
has length 64. Preserve the existing provenance identifier assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 94cfac87-5e29-49b5-af59-7d91f6547c09

📥 Commits

Reviewing files that changed from the base of the PR and between 83838ec and d20096f.

📒 Files selected for processing (2)
  • swiftpkg/Provenance.swift
  • swiftpkgTests/ProvenanceTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • swiftpkg/Provenance.swift

@jordancalhoun
jordancalhoun changed the base branch from main to next August 2, 2026 14:04
@jordancalhoun
jordancalhoun merged commit 4712d87 into codecarton:next Aug 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants