Swift CLI for macOS CI/CD tooling. Modular SPM package, Swift 6.1, macOS 15+.
| Module | Purpose |
|---|---|
ENV |
@dynamicMemberLookup wrapper around ProcessInfo environment |
CI |
CI platform abstraction: GitHub Actions, Azure Pipelines, local |
Shell |
Process execution, which, mise CLI integration |
EnvSubst |
POSIX-compatible $VAR / ${VAR:-default} substitution engine |
SecretsInterface |
Protocol layer for secret provider implementations |
SecretsInterfaceTesting |
Mock implementations for use in tests |
HashiCorpVaultReader |
HashiCorp Vault HTTP client (KV v1/v2, AWS engine) |
ImportSecrets |
Loads secrets from 1Password CLI or HashiCorp Vault via YAML config |
ExportSecrets |
Writes secrets to dotenv / mise / stdout / CI env |
ObfuscateSecrets |
Envsubst + swift-confidential obfuscation (macOS only) |
SemanticVersion |
SemVer 2.0 type + build-tool plugin embedding .config/semantic-version/version |
SharedLogger |
swift-log singleton; all targets depend on it automatically |
XCResultParsing |
xcresulttool CLI wrapper + Codable types for coverage and test results |
XUnitReport |
xUnit XML parser — XUnitReport.load(from:) → XUnitReport/TestSuite/TestCase/Failure |
LLVMCoverageParser |
LLVM coverage JSON parser — LLVMCoverage.load(from:) → LLVMCoverage/File/Summary/Stat |
TestsCoverageCommand |
mpct tests coverage parse — exports and prints coverage report |
TestsResultsCommand |
mpct tests results parse — fetches and prints test result summary |
TestsOTELReporter |
Neutral TestRun / LineCoverage models + OTLP/HTTP JSON metrics payload and push (no OpenTelemetry SDK dependency) |
TestsOTELReporterCommand |
mpct tests report otel — parses a report and line coverage and pushes their metrics |
mpct [--verbose]
envsubst Substitute env vars in a file or stdin
secrets
export Fetch secrets → --destination (dotenv | mise | stdout | ci)
obfuscate Envsubst + swift-confidential (macOS only)
tests
coverage
parse Export xcresult coverage → CoverageReport
results
parse Fetch xcresult test results → TestResults
report
otel Push test and line coverage metrics to an OpenTelemetry collector (OTLP/HTTP)
Inputs: --xcresult (macOS only; reports tests and coverage, so it excludes the others), or
--xunit and/or --llvm-line-coverage (an llvm-cov export JSON; the Linux path). At least one is required.
Endpoint and headers fall back to OTEL_EXPORTER_OTLP_ENDPOINT / OTEL_EXPORTER_OTLP_HEADERS; there is
no default endpoint. OTEL_EXPORTER_OTLP_ENDPOINT is a base URL — v1/metrics is appended, per the
OTLP spec.
Emits gauges/histograms, all with millisecond durations: cicd.test.case.count
(attributed by test.case.result.status), cicd.test.case.duration, cicd.test.suite.duration (summed case
durations per suite, per-suite only), cicd.test.run.duration. The suite and run duration carry
test.suite.run.status (success / failure / skipped; timed_out / aborted cannot be told apart in a report).
Resource attributes come from CI.current.repository / .cicd, plus service.name (OTEL_SERVICE_NAME, default
macpaw-test, shared with the Playwright reporter). There is no service.instance.id: Mimir makes it the instance
label, so a per-run value would start new series every run; runs share a series and add one sample each. The filterable ones (repository, pipeline,
stage, job) are repeated as data-point attributes, since backends do not promote resource attributes to
metric labels by default; run id / number / URL stay on the resource only (a label per run would explode series).
mpct logs the CI context it attaches before each export.
Test case names are never metric labels (a series per test grows without bound). Each case is one OTLP log record
(test.case.name, test.suite.name, test.case.result.status, test.case.duration in ms; severity INFO / ERROR /
WARN for pass / fail / skip; body is the name plus the failure message), sent after the metrics in batches of 500 with
the same resource. Destination: --loki-otlp-endpoint / LOKI_OTLP_ENDPOINT is a base URL like the OTLP
one: v1/logs is appended, so it must carry Loki's own prefix (http://loki:3100/otlp); unset, logs go to the OTLP
endpoint. --loki-otlp-header /
LOKI_OTLP_HEADERS replace the metrics headers for logs when set. A failing logs export fails the command (metrics are
already delivered); there is no opt-out flag.
Line coverage is one more gauge, cicd.test.coverage.line.count (unit {line}): per target, a covered and an
uncovered data point (test.coverage.target.name, test.coverage.line.status). Their sum is the executable
line count; there is no run-level total. These names are MacPaw extensions (no OTel registry entry).
--no-line-coverage skips it; an xcresult without coverage data warns and reports tests only. xcresult drops
*.xctest targets and LLVM drops Tests/ and .build/ paths unless --include-test-targets. LLVM has no
targets, so the target is the directory after Sources/; other files go under other.
Use mise tasks run (not mise run). Nested tasks use : as separator.
mise tasks run build # Build
mise tasks run test # Run all tests
mise tasks run test:swift # Run Swift tests only
mise tasks run fixtures:generate # Regenerate lcov, llvm-diff, xcresult fixtures
mise tasks run format # Format Swift, YAML, shell
mise tasks run lint # Lint Swift, YAML, shell
make bootstrap # First-time setup (installs mise and tools)All generated fixtures live under Tests/*/fixtures/ and are regenerated with:
mise tasks run fixtures:generateThe task builds from Tests/fixtures/TestPackage (two source targets, a recording test target) and Tests/fixtures/Diffs/Before + After. It regenerates:
Tests/LLVMCoverageParserTests/fixtures/lcov.jsonTests/LLVMCoverageParserTests/fixtures/llvm-diff-before.jsonTests/LLVMCoverageParserTests/fixtures/llvm-diff-after.jsonTests/XCResultParsingTests/fixtures/test-package.xcresultTests/XCResultParsingTests/fixtures/coverage-diff.jsonTests/XUnitReportTests/fixtures/test-package-xUnit-swift-testing.xml
The xUnit XML is generated with MPCT_TESTS_RECORD_FAILURES=1, which enables RecordingTests so the fixture contains <failure> and <skipped> elements. Since Swift 6.4.0, swift test --xunit-output writes the file even when tests fail, and emits one <testsuite> per test target.
SwiftPM 6.4's own codecov/*.json covers only one test binary, so the generator instead runs llvm-cov export over every .xctest binary with the merged default.profdata.
- Every target is created with
targetBundle(...)inPackage.swift, which automatically addsSharedLoggeras a dependency. - Command targets (e.g.,
ExportSecretsCommand) also getArgumentParseradded automatically. - Conditional (macOS-only) targets use
#if os(macOS)/.when(platforms: [.macOS]). - Multiple Package manifests: the repo has
Package.swiftandPackage@swift-6.3.swift. When adding or modifying targets, update ALL manifests. Check withls Package*.swift. - All public declarations must have
///doc comments — types, properties, methods, and initializers.
Always invoke the /new-module skill before writing tests to pick up the current conventions. Key rules:
- Name the system under test
sut. - Method name:
test_functionOrProperty_testExplanation. @Testlabel:"functionOrProperty testExplanation"(notest_prefix).- Structure each test with
// GIVEN,// WHEN,// THENinline comments. - Test outputs, not internal behaviour.
Bump the version in .config/semantic-version/version, update README.md, and merge to main. CI builds and publishes the release automatically.
Always invoke the relevant skill before starting a task. Available skills:
/new-module— scaffold a new SPM module (includes Package manifest, source, test conventions, and doc comment rules)
If skills don't appear in your agent, run:
./scripts/tools/ai/ai.bash