Skip to content
Open
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
144 changes: 109 additions & 35 deletions .github/workflows/release-swift-client.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,19 @@ on:
- patch
- minor
- major
prerelease:
description: 'Create prerelease (beta tag)?'
required: false
default: true
type: boolean

concurrency:
group: sdk-release
cancel-in-progress: false

jobs:
release:
runs-on: macos-latest
runs-on: macos-26
permissions:
contents: write
id-token: write
Expand All @@ -35,9 +40,21 @@ jobs:
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '22.13.1'
registry-url: 'https://registry.npmjs.org'

- name: Show Xcode version
run: xcodebuild -version
- name: Upgrade npm for Trusted Publishers
run: npm install -g npm@11.5.1

- name: Install pnpm
run: |
npm install -g corepack@latest
corepack enable
corepack prepare pnpm@11.3.0 --activate

- name: Select Xcode version
run: |
sudo xcode-select -s /Applications/Xcode_26.6.app
xcodebuild -version

- name: Configure git
run: |
Expand All @@ -53,12 +70,8 @@ jobs:
id: current_version
working-directory: ./clients/swift
run: |
# Extract the latest released semver entry. Ignore [Unreleased].
CURRENT_VERSION=$(grep -m 1 -E '^## \[[0-9]+\.[0-9]+\.[0-9]+\]' CHANGELOG.md | sed 's/## \[\(.*\)\].*/\1/' || true)
if [ -z "$CURRENT_VERSION" ]; then
CURRENT_VERSION="0.0.0"
fi
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
CURRENT_VERSION=$(node -p "require('./package.json').version")
echo "version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"

- name: Get previous release tag
id: previous_tag
Expand All @@ -69,33 +82,37 @@ jobs:
fi
echo "tag=$PREV_TAG" >> $GITHUB_OUTPUT

- name: Calculate new version
- name: Bump version
id: new_version
working-directory: ./clients/swift
run: |
CURRENT="${{ steps.current_version.outputs.version }}"

# Validate version format (X.Y.Z)
if ! [[ "$CURRENT" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Invalid version format '$CURRENT'. Expected X.Y.Z"
exit 1
if [ "${{ github.event.inputs.prerelease }}" == "true" ]; then
if [[ "${{ steps.current_version.outputs.version }}" == *-beta.* ]]; then
NEW_VERSION=$(npm version prerelease --preid=beta --no-git-tag-version | sed 's/v//')
else
NEW_VERSION=$(npm version pre${{ github.event.inputs.version_type }} --preid=beta --no-git-tag-version | sed 's/v//')
fi
else
NEW_VERSION=$(npm version ${{ github.event.inputs.version_type }} --no-git-tag-version | sed 's/v//')
fi

IFS='.' read -r -a parts <<< "$CURRENT"

case "${{ github.event.inputs.version_type }}" in
major)
NEW_VERSION="$((parts[0] + 1)).0.0"
;;
minor)
NEW_VERSION="${parts[0]}.$((parts[1] + 1)).0"
;;
patch)
NEW_VERSION="${parts[0]}.${parts[1]}.$((parts[2] + 1))"
;;
esac

echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "tag=swift/v$NEW_VERSION" >> $GITHUB_OUTPUT
echo "spm_tag=v$NEW_VERSION" >> $GITHUB_OUTPUT

- name: Verify release tags are available
run: |
for tag in \
"${{ steps.new_version.outputs.tag }}" \
"${{ steps.new_version.outputs.spm_tag }}"
do
if git rev-parse "refs/tags/$tag" >/dev/null 2>&1; then
echo "Release tag $tag already exists"
exit 1
fi
done

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Generate changelog
continue-on-error: true
Expand Down Expand Up @@ -173,18 +190,74 @@ jobs:
working-directory: ./clients/swift
run: swift test

- name: Check preview CLI package
working-directory: ./clients/swift
run: pnpm run check

- name: Run stock preview capture test
working-directory: ./clients/swift
run: |
xcrun simctl shutdown all
DEVICE_UDID="$(xcrun simctl list devices available --json | node --input-type=module -e '
let input = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", chunk => input += chunk);
process.stdin.on("end", () => {
let payload = JSON.parse(input);
let device = Object.entries(payload.devices)
.filter(([runtime]) => runtime.includes(".SimRuntime.iOS-"))
.flatMap(([, devices]) => devices)
.find(candidate => candidate.isAvailable !== false);
if (!device) process.exit(1);
process.stdout.write(device.udid);
});
')"
xcrun simctl boot "$DEVICE_UDID"
xcrun simctl bootstatus "$DEVICE_UDID" -b
VIZZLY_SIMULATOR_UDID="$DEVICE_UDID" pnpm run test:previews:e2e

- name: Verify npm package version is unpublished
working-directory: ./clients/swift
run: |
if npm view @vizzly-testing/swift@${{ steps.new_version.outputs.version }} version >/dev/null 2>&1; then
echo "@vizzly-testing/swift@${{ steps.new_version.outputs.version }} is already published"
exit 1
fi

- name: Pack npm package
id: pack
working-directory: ./clients/swift
run: |
PACK_FILE=$(npm pack --ignore-scripts)
echo "file=$PACK_FILE" >> $GITHUB_OUTPUT

- name: Configure git identity
run: |
git config --local user.email "${{ secrets.GIT_USER_EMAIL }}"
git config --local user.name "${{ secrets.GIT_USER_NAME }}"

- name: Commit and push changes
run: |
git add clients/swift/CHANGELOG.md
git add clients/swift/package.json clients/swift/CHANGELOG.md
git commit -m "🔖 Swift client v${{ steps.new_version.outputs.version }}"
git push origin main
git tag "${{ steps.new_version.outputs.tag }}"
git push origin "${{ steps.new_version.outputs.tag }}"
git tag "${{ steps.new_version.outputs.spm_tag }}"
git push --atomic origin \
main \
"${{ steps.new_version.outputs.tag }}" \
"${{ steps.new_version.outputs.spm_tag }}"

- name: Publish preview CLI package to npm
working-directory: ./clients/swift
run: |
npm config delete //registry.npmjs.org/:_authToken 2>/dev/null || true
rm -f ~/.npmrc 2>/dev/null || true
npm config set registry https://registry.npmjs.org/
if [ "${{ github.event.inputs.prerelease }}" == "true" ]; then
npm publish "${{ steps.pack.outputs.file }}" --provenance --access public --tag beta
else
npm publish "${{ steps.pack.outputs.file }}" --provenance --access public
fi

- name: Read changelog for release
id: release_notes
Expand All @@ -204,7 +277,8 @@ jobs:
tag_name: ${{ steps.new_version.outputs.tag }}
name: 📱 Swift SDK v${{ steps.new_version.outputs.version }}
body: ${{ steps.release_notes.outputs.notes }}
files: ./clients/swift/${{ steps.pack.outputs.file }}
draft: false
prerelease: false
prerelease: ${{ github.event.inputs.prerelease }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3 changes: 2 additions & 1 deletion .github/workflows/sdk-unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,8 @@ jobs:
# Swift SDK - uses the Xcode version configured by the hosted runner
swift:
name: Swift SDK
runs-on: macos-latest
# Xcode 16.2 and 16.4 are both installed on this pinned image.
runs-on: macos-15
timeout-minutes: 8
needs: changes
if: needs.changes.outputs.swift == 'true'
Expand Down
13 changes: 13 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ let package = Package(
.library(
name: "VizzlyXCTest",
targets: ["VizzlyXCTest"]),
.library(
name: "VizzlyPreviewRuntime",
type: .dynamic,
targets: ["VizzlyPreviewRuntime"]),
],
targets: [
.target(
Expand All @@ -26,6 +30,15 @@ let package = Package(
name: "VizzlyXCTest",
dependencies: ["Vizzly"],
path: "clients/swift/Sources/VizzlyXCTest"),
.target(
name: "CVizzlyPreviewRuntime",
dependencies: [],
path: "clients/swift/Sources/CVizzlyPreviewRuntime",
publicHeadersPath: "include"),
.target(
name: "VizzlyPreviewRuntime",
dependencies: ["CVizzlyPreviewRuntime"],
path: "clients/swift/Sources/VizzlyPreviewRuntime"),
.testTarget(
name: "VizzlyTests",
dependencies: ["Vizzly", "VizzlyXCTest"],
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,17 @@ Or upload an existing folder of screenshots:
vizzly upload ./screenshots --threshold 2 --min-cluster-size 4 --batch-size 10 --upload-timeout 60000
```

For iOS apps, the Swift plugin can render the stock SwiftUI `#Preview`
declarations already in the app target:

```bash
pnpm add --save-dev @vizzly-testing/cli@beta @vizzly-testing/swift@beta
pnpm exec vizzly previews
```

See the [SwiftUI preview guide](clients/swift/PREVIEWS.md) for the supported
Xcode and Simulator setup.

`--batch-size` controls how many screenshots are uploaded per request.
`--upload-timeout` controls the upload client's timeout, including how long
`--wait` polls for build processing.
Expand Down Expand Up @@ -237,6 +248,7 @@ export default {
| `vizzly run "cmd"` | Run tests with cloud build and review integration. |
| `vizzly context ...` | Fetch visual context for builds, comparisons, screenshots, and review queues. |
| `vizzly upload <dir>` | Upload an existing folder of screenshots. |
| `vizzly previews [container]` | Render and upload stock SwiftUI previews. |
| `vizzly preview <dir>` | Upload static build output for in-context review. |
| `vizzly approve <comparison-id>` | Approve a visual comparison. |
| `vizzly reject <comparison-id>` | Reject a visual comparison with a reason. |
Expand Down
2 changes: 2 additions & 0 deletions clients/swift/.gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Swift Package Manager
.build/
*.xcodeproj
!Fixtures/PreviewFixture/PreviewFixture.xcodeproj/
!Fixtures/PreviewFixture/PreviewFixture.xcodeproj/project.pbxproj
.swiftpm/

# Xcode
Expand Down
46 changes: 46 additions & 0 deletions clients/swift/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Added a `vizzly previews` plugin and native Simulator runtime
that render existing stock SwiftUI `#Preview` declarations without Xcode MCP.
- Added an iOS fixture that exercises app-module discovery, a named asset,
linked-runtime capture, preview traits, isolated failures, PNG output, and
manifest generation.
- Added a dynamic `VizzlyPreviewRuntime` Swift Package product that Xcode builds,
embeds, and signs as part of the app target.
- Added conservative booted iOS Simulator detection, with an explicit choice
required when more than one Simulator is booted.
- Added conservative Xcode scheme detection, repeatable managed output, a
per-preview capture timeout, and clearer unsupported-preview failures.
- Added npm packaging, CI checks, and release publishing for the Swift preview
CLI plugin.
- Added automatic local TDD delivery for rendered preview PNGs, including
comparison metadata for the Simulator, viewport, SwiftUI view, Xcode, and
scheme.
- Added cloud build creation, screenshot upload, flush, finalization, and build
URL reporting through the stable Vizzly plugin API.
- Added `--no-upload`, local-only fallback, and upload outcomes in the preview
manifest.
- Added fixed-layout and portrait or landscape trait rendering with exact
output dimensions.
- Added per-preview failure isolation. Successful screenshots are kept and
uploaded before an incomplete capture exits with a failure.
- Added `VizzlyPreviewRuntime.isCapturing` so apps can skip unsafe or unwanted
startup services during preview launches.
- Added an explicit CLI peer version for the isolated screenshot service used
by preview uploads.

### Fixed

- Replaced CLI-side runtime compilation, app-bundle mutation, ad hoc re-signing,
and `DYLD_INSERT_LIBRARIES` with a normal Swift Package integration.
- Fixed app executable discovery when Xcode does not emit a debug dylib.
- Fixed Swift preview configuration so command options only override values
explicitly provided in `vizzly.config.js`.
- Fixed the Simulator runtime's platform and scene lifecycle boundaries.
- Fixed preview upload discovery for the TDD daemon's serialized port format
and normalized stock preview names for Vizzly's screenshot contract.
- Fixed managed output validation so missing or duplicate preview files are
never treated as safe to replace.
- Fixed preview uploads so both supported `VIZZLY_FAIL_ON_DIFF` values, `true`
and `1`, behave consistently.

## [0.1.0] - 2026-06-01

### What's Changed
Expand Down
Loading
Loading