Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
name: Nightly

on:
push:
pull_request:
branches:
- main
schedule:
- cron: "0 3 * * *"
types:
- closed
workflow_dispatch:

permissions:
Expand All @@ -17,6 +17,7 @@ concurrency:

jobs:
build-macos:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.merged == true }}
name: Build nightly macOS (${{ matrix.arch }})
runs-on: macos-15
strategy:
Expand Down Expand Up @@ -61,6 +62,7 @@ jobs:
path: dist/*

publish-nightly:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.merged == true }}
name: Publish Nightly Release
runs-on: ubuntu-latest
needs: [build-macos]
Expand All @@ -82,7 +84,9 @@ jobs:
run: |
set -euo pipefail
NIGHTLY_VERSION="$(date -u +'%Y.%m.%d').${GITHUB_RUN_NUMBER}"
NIGHTLY_COMMIT="${{ github.event.pull_request.merge_commit_sha || github.sha }}"
echo "NIGHTLY_VERSION=${NIGHTLY_VERSION}" >> "${GITHUB_ENV}"
echo "NIGHTLY_COMMIT=${NIGHTLY_COMMIT}" >> "${GITHUB_ENV}"

- name: Publish nightly assets
uses: softprops/action-gh-release@v2
Expand All @@ -93,14 +97,15 @@ jobs:
make_latest: false
generate_release_notes: false
body: |
Nightly build for commit `${{ github.sha }}`.
Nightly build for commit `${{ env.NIGHTLY_COMMIT }}`.
Version: `${{ env.NIGHTLY_VERSION }}`
files: |
dist/*.tar.gz
dist/*.sha256
overwrite_files: true

update-homebrew-tap-nightly:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.merged == true }}
name: Update Homebrew Tap (nightly)
runs-on: ubuntu-latest
needs: [publish-nightly]
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ Release automation included:
- Builds `snapshot-report` for macOS `arm64` and `x86_64` on tag push (`v*`)
- Uploads artifacts to GitHub Releases
- Updates `oscarcv/tap` formula automatically
- `.github/workflows/nightly.yml`
- Runs on PR merges into `main`
- Publishes/updates a moving `nightly` prerelease (`arm64` + `x86_64`)
- Updates `oscarcv/tap` nightly formula (`snapshot-report-nightly`)

## Examples

Expand Down Expand Up @@ -157,6 +161,18 @@ swift run snapshot-report \
--output .artifacts/report
```

### Verbose diagnostics

Use `--verbose` to print detailed processing diagnostics (inputs, phase progress, odiff usage, and timings):

```bash
swift run snapshot-report \
--input-dir .artifacts/snapshot-runs \
--output .artifacts/report \
--format json,junit,html \
--verbose
```

### odiff pixel diff

[odiff](https://github.com/dmtrKovalenko/odiff) is a SIMD-accelerated image diff tool that produces highlighted difference images. Install it first:
Expand Down
62 changes: 57 additions & 5 deletions Sources/snapshot-report/CLI/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import SnapshotReportCLI

struct CLI {
static func run() throws {
CLIUI.header("snapshot-report")
let arguments = Array(CommandLine.arguments.dropFirst())

if arguments.first == "inspect" {
Expand All @@ -19,10 +18,14 @@ struct CLI {
}

let options = try parse(arguments: arguments)
CLIUI.setVerbose(options.verbose)
CLIUI.header("snapshot-report")
try FileManager.default.createDirectory(at: options.outputDirectory, withIntermediateDirectories: true)

let startedAt = Date()
CLIUI.step("Resolving input files")
let resolvedInputs = try resolveInputs(options: options)
CLIUI.debug("Resolved \(resolvedInputs.count) JSON inputs and \(options.xcresultInputs.count) xcresult inputs")

CLIUI.step("Loading JSON reports (\(resolvedInputs.count))")
let reports = try _loadJSONReports(inputs: resolvedInputs, jobs: options.jobs) { completed, total, input in
Expand All @@ -36,13 +39,16 @@ struct CLI {

CLIUI.step("Merging report data")
let mergedReport = SnapshotReportAggregator.merge(reports: reports + xcresultReports, name: options.reportName)
CLIUI.debug("Merged report: \(mergedReport.summary.total) tests (\(mergedReport.summary.passed) passed, \(mergedReport.summary.failed) failed, \(mergedReport.summary.skipped) skipped)")

let effectiveOdiffPath = options.odiffPath ?? _resolveOnPATH("odiff")
let finalReport: SnapshotReport
if let odiffPath = effectiveOdiffPath {
CLIUI.step("Generating odiff attachments")
CLIUI.debug("Using odiff binary: \(odiffPath)")
finalReport = OdiffProcessor(odiffBinaryPath: odiffPath).process(report: mergedReport)
} else {
CLIUI.debug("odiff not found; skipping diff enrichment")
finalReport = mergedReport
}

Expand All @@ -57,6 +63,8 @@ struct CLI {
CLIUI.progress("Output \(completed)/\(total): \(format.rawValue)")
}

let elapsed = Date().timeIntervalSince(startedAt)
CLIUI.debug(String(format: "Total processing time: %.3fs", elapsed))
CLIUI.success("Generated report \(options.formats.map(\.rawValue).joined(separator: ", ")) at \(options.outputDirectory.path)")
}

Expand All @@ -70,6 +78,7 @@ struct CLI {
var reportName: String?
var odiffPath: String?
var jobs = max(1, ProcessInfo.processInfo.activeProcessorCount)
var verbose = false

var index = 0
while index < arguments.count {
Expand Down Expand Up @@ -122,6 +131,8 @@ struct CLI {
throw SnapshotReportError.invalidInput("--jobs must be a positive integer")
}
jobs = parsed
case "--verbose", "-v":
verbose = true
default:
throw SnapshotReportError.invalidInput("Unknown argument: \(argument)")
}
Expand All @@ -142,7 +153,8 @@ struct CLI {
htmlTemplate: htmlTemplate,
reportName: reportName,
odiffPath: odiffPath,
jobs: jobs
jobs: jobs,
verbose: verbose
)
}

Expand Down Expand Up @@ -191,6 +203,7 @@ struct CLI {
--name <string> Override merged report name
--odiff <path> Path to odiff binary (default: auto-detect on PATH)
--jobs <n> Max parallel xcresult reads (default: CPU count)
-v, --verbose Enable diagnostic output
--help Show help
"""
)
Expand All @@ -206,6 +219,7 @@ struct CLI {
let reportName: String?
let odiffPath: String?
let jobs: Int
let verbose: Bool
}
}

Expand Down Expand Up @@ -421,17 +435,34 @@ private final class ReportWriteState: @unchecked Sendable {
private enum CLIUI {
private static let lock = NSLock()
private static let prefix = "[snapshot-report]"
private static let state = VerboseState()

static func setVerbose(_ value: Bool) {
state.set(value)
}

static func header(_ title: String) {
log("\(prefix) \(title)")
if verboseEnabled() {
log("\(prefix) \(title)")
}
}

static func step(_ message: String) {
log("\(prefix) step: \(message)")
if verboseEnabled() {
log("\(prefix) step: \(message)")
}
}

static func progress(_ message: String) {
log("\(prefix) progress: \(message)")
if verboseEnabled() {
log("\(prefix) progress: \(message)")
}
}

static func debug(_ message: String) {
if verboseEnabled() {
log("\(prefix) debug: \(message)")
}
}

static func success(_ message: String) {
Expand All @@ -443,6 +474,27 @@ private enum CLIUI {
print(message)
lock.unlock()
}

private static func verboseEnabled() -> Bool {
state.get()
}
}

private final class VerboseState: @unchecked Sendable {
private let lock = NSLock()
private var value = false

func set(_ newValue: Bool) {
lock.lock()
value = newValue
lock.unlock()
}

func get() -> Bool {
lock.lock()
defer { lock.unlock() }
return value
}
}

private func _resolveOnPATH(_ name: String) -> String? {
Expand Down