Skip to content

Post a vendor-neutral run summary on completion#5

Merged
rodchristiansen merged 3 commits into
mainfrom
feat/agnostic-reporting
Jun 16, 2026
Merged

Post a vendor-neutral run summary on completion#5
rodchristiansen merged 3 commits into
mainfrom
feat/agnostic-reporting

Conversation

@rodchristiansen

Copy link
Copy Markdown
Contributor

Problem

BootstrapMate's run status was local-only (/Library/Preferences/com.github.bootstrapmate.plist + status.json), so answering "did this Mac provision cleanly?" across a fleet meant an SSH/ARD expedition. The audit flagged this as the highest-leverage feature given existing reporting infrastructure.

Fix

Add an optional, vendor-neutral run-summary POST. When reportingUrl is configured, ReportManager posts a JSON summary on completion. The payload is deliberately not tied to any one backend (ReportMate, MunkiReport, custom collector) — any endpoint accepting a JSON POST can consume it.

Payload fields: tool, platform, version, runId, success, startTime/endTime, durationSeconds, architecture, hostname, serialNumber, manifestUrl, and per-phase outcomes (reusing the data StatusManager already persists).

The POST is best-effort: failures are logged and never block or fail the run (30s timeout).

Configuration

Managed preferences (com.github.bootstrapmate): reportingUrl (string), reportingHeader (optional Authorization value). CLI: --reporting-url.

Tests

swift build + swift test pass. Adds a ReportManager suite covering payload shape and JSON serialization (payload assembly is a pure, testable function). README + example mobileconfig updated.

Notes

This is the macOS half; the Windows equivalent is in progress and will emit the same schema for one fleet-wide model.

BootstrapMate status was local-only (plist + status.json), so checking
whether a Mac provisioned cleanly meant an SSH/ARD expedition. Add an
optional reporting POST: when reportingUrl is configured, a JSON run
summary is sent on completion.

The payload is intentionally backend-agnostic (not ReportMate-specific) —
any endpoint that accepts a JSON POST can consume it. It includes runId,
version, success, start/end/duration, architecture, hostname, serial
number, manifest URL, and per-phase outcomes (reusing the data
StatusManager already persists). The POST is best-effort: failures are
logged and never block or fail the run.

Configurable via managed preferences (reportingUrl, reportingHeader) and
the --reporting-url CLI flag. swift build and swift test pass.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds optional fleet reporting by posting a vendor-neutral JSON run summary to a configured endpoint after a BootstrapMate run completes, enabling centralized “did provisioning succeed?” visibility without local inspection.

Changes:

  • Introduces ReportManager to assemble a run-summary payload (core metadata + per-phase outcomes) and POST it to reportingUrl.
  • Adds configuration support for reportingUrl / reportingHeader (managed preferences) and --reporting-url (CLI).
  • Adds tests for payload shape/JSON serialization and updates README + example mobileconfig.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
Sources/core/Managers/ReportManager.swift New manager to build and POST the run-summary payload.
Sources/core/Managers/IAOrchestrator.swift Sends the report at the end of a run before reboot/cleanup.
Sources/core/Managers/ConfigManager.swift Adds reporting config fields, preference loading, and CLI overrides.
Sources/cli/bootstrapmate.swift Adds --reporting-url flag and wires it into ConfigManager.
Tests/BootstrapMateCoreTests/BootstrapMateCoreTests.swift Adds unit tests for payload shape and JSON serialization.
README.md Documents reporting configuration and payload fields.
examples/config.mobileconfig Adds example keys for reportingUrl and reportingHeader.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +108 to +125
let semaphore = DispatchSemaphore(value: 0)
let task = URLSession.shared.dataTask(with: request) { _, response, error in
defer { semaphore.signal() }
if let error = error {
Logger.warning("Reporting POST failed: \(error.localizedDescription)")
return
}
if let http = response as? HTTPURLResponse {
if (200...299).contains(http.statusCode) {
Logger.info("Run summary reported (HTTP \(http.statusCode))")
} else {
Logger.warning("Reporting endpoint returned HTTP \(http.statusCode)")
}
}
}
task.resume()
_ = semaphore.wait(timeout: .now() + 35)
}
Comment on lines +82 to +95
private func collectPhases() -> [String: [String: Any]] {
var phases: [String: [String: Any]] = [:]
for phase in [InstallationPhase.preflight, .setupAssistant, .userland] {
guard let status = StatusManager.shared.getPhaseStatus(phase: phase) else { continue }
phases[phase.rawValue] = [
"stage": status.stage.rawValue,
"exitCode": status.exitCode,
"startTime": status.startTime,
"completionTime": status.completionTime,
"lastError": status.lastError
]
}
return phases
}
Comment on lines +46 to +47
Logger.info("Posting run summary to reporting endpoint: \(urlString)")
postJSON(body, to: url, authHeader: config.reportingHeader)
Logger.debug(" silentMode: \(config.silentMode)")
Logger.debug(" verboseMode: \(config.verboseMode)")
Logger.debug(" installPath: \(getInstallPath())")
Logger.debug(" reportingUrl: \(config.reportingUrl ?? "not set")")
Comment thread README.md Outdated
| `reportingUrl` | string | Endpoint to POST the run summary to. When unset, no report is sent. |
| `reportingHeader` | string | Optional `Authorization` header value sent with the POST. |

The POST is best-effort: failures are logged and never block or fail the run. Payload fields include `tool`, `platform`, `version`, `runId`, `success`, `startTime`/`endTime`, `durationSeconds`, `architecture`, `hostname`, `serialNumber`, `manifestUrl`, and per-phase outcomes (`preflight`/`setupassistant`/`userland` with stage, exit code, and any error).
Comment thread Sources/core/Managers/ConfigManager.swift
- Redact the reporting endpoint in logs (scheme/host/path only) and stop
  logging the raw reportingUrl in config debug output, so tokens embedded
  in the URL never reach the logs.
- Normalize per-phase timestamps to ISO-8601 so the whole payload uses one
  timestamp format (falls back to the original string if unparseable).
- Correct docs/comments: the POST is bounded by a short timeout (now 15s)
  rather than 'never blocking', and document the actual per-phase payload
  keys (Preflight/SetupAssistant/Userland).
# Conflicts:
#	README.md
#	Sources/cli/bootstrapmate.swift
#	Sources/core/Managers/ConfigManager.swift
#	Tests/BootstrapMateCoreTests/BootstrapMateCoreTests.swift
#	examples/config.mobileconfig
@rodchristiansen rodchristiansen merged commit 58880b4 into main Jun 16, 2026
1 check passed
@rodchristiansen rodchristiansen deleted the feat/agnostic-reporting branch June 16, 2026 01:58
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