Skip to content
Closed
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
69 changes: 69 additions & 0 deletions .github/workflows/release-tag.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Release Tagging

on:
pull_request:
types: [closed]
branches:
- main
- development

jobs:
create-tag:
if: github.event.pull_request.merged == true && startsWith(github.head_ref, 'release/')
runs-on: ubuntu-latest
outputs:
version: ${{ steps.extract-version.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Git identity
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "actions@github.com"

- name: Extract version from branch name
id: extract-version
run: |
BRANCH_NAME="${{ github.head_ref }}"
VERSION=${BRANCH_NAME#release/}
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Extracted version: $VERSION"

- name: Verify Version.swift contains correct version
run: |
VERSION_IN_FILE=$(grep -o 'private static let version = "[^"]*"' SplitThin/Common/Version.swift | cut -d'"' -f2)
if [ "$VERSION_IN_FILE" != "${{ steps.extract-version.outputs.version }}" ]; then
echo "❌ Error: Version in Version.swift ($VERSION_IN_FILE) does not match branch version (${{ steps.extract-version.outputs.version }})"
exit 1
fi
echo "✅ Version.swift contains correct version: $VERSION_IN_FILE"

- name: Create tag
env:
VERSION: ${{ steps.extract-version.outputs.version }}
run: |
if git rev-parse -q --verify "refs/tags/$VERSION" >/dev/null; then
echo "ℹ️ Tag $VERSION already exists locally — skipping create."
else
echo "🏷️ Creating tag $VERSION..."
git tag -a "$VERSION" -m "Release $VERSION"
fi
git push origin "$VERSION"

- name: Verify tag in remote
run: |
echo "✅ Verifying tag ${{ steps.extract-version.outputs.version }} exists in remote..."
sleep 5
git fetch --tags

if git ls-remote --tags origin | grep -q "refs/tags/${{ steps.extract-version.outputs.version }}$"; then
echo "✅ Tag ${{ steps.extract-version.outputs.version }} successfully created in remote"
else
echo "❌ Failed to verify tag ${{ steps.extract-version.outputs.version }} in remote"
exit 1
fi
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,42 @@ pipeline:
set -euo pipefail
chmod +x .harness/scripts/install-deps.sh
.harness/scripts/install-deps.sh
- step:
type: Run
name: Build and Test (Swift 6 strict concurrency)
identifier: swift6_strict_concurrency_check
spec:
shell: Sh
command: |
set -euo pipefail
# Package.swift stays on swift-tools-version 5.5 (toolchain compat), so this
# is the only place that actually validates the SDK against Swift 6 strict mode.
swift build -Xswiftc -swift-version -Xswiftc 6

# Headless macOS runners have no unlocked login keychain, so generic-password
# items never round-trip and KeychainCredentialStorage persistence tests fail.
# Create and unlock a temporary default keychain for this session.
KEYCHAIN_PATH="$(pwd)/ci.keychain-db"
security create-keychain -p "" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "" "$KEYCHAIN_PATH"
security default-keychain -s "$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH"

swift test -Xswiftc -swift-version -Xswiftc 6

- step:
type: Run
name: Build and Test (Swift 5)
identifier: swift5
spec:
shell: Sh
command: |
set -euo pipefail
# Swift 5 is the mode we actually ship (Package.swift is swift-tools-version 5.5),
# so validate the shipped semantics first, then re-run under Swift 6 strict for
# forward-compat.
swift test
- step:
type: RunTests
name: Run unit tests
Expand Down
2 changes: 1 addition & 1 deletion SplitThin/Common/Version.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Foundation

enum Version {
private static let sdkPlatform = "iOSThin"
private static let version = "1.0.0"
private static let version = "1.0.2-rc1"

static var semantic: String {
version
Expand Down
26 changes: 16 additions & 10 deletions SplitThin/Events/SplitEventsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,14 @@ final class DefaultSplitEventsManager: SplitEventsManager, @unchecked Sendable {
guard let self else { return }
self.appendListener(listener)

let boxedListener = UncheckedSendableBox(value: listener)

// Sticky events: replay already-fired state to late subscribers
if let metadata = self.getReadyMetadata() {
DispatchQueue.main.async { listener.onReady(metadata) }
DispatchQueue.main.async { boxedListener.value.onReady(metadata) }
}
if let metadata = self.getCacheMetadata() {
DispatchQueue.main.async { listener.onReadyFromCache(metadata) }
DispatchQueue.main.async { boxedListener.value.onReadyFromCache(metadata) }
}
}
}
Expand Down Expand Up @@ -135,34 +137,38 @@ final class DefaultSplitEventsManager: SplitEventsManager, @unchecked Sendable {
guard !isSdkReadyFired() else { return }
setReadyMetadata(metadata)

getListeners().forEach { listener in
DispatchQueue.main.async { listener.onReady(metadata) }
getListeners().forEach { listener in
let boxedListener = UncheckedSendableBox(value: listener)
DispatchQueue.main.async { boxedListener.value.onReady(metadata) }
}
}

private func triggerReadyFromCache(_ metadata: SdkReadyFromCacheMetadata) {
guard !isSdkReadyFromCacheFired() else { return }
setCacheMetadata(metadata)

getListeners().forEach { listener in
DispatchQueue.main.async { listener.onReadyFromCache(metadata) }
getListeners().forEach { listener in
let boxedListener = UncheckedSendableBox(value: listener)
DispatchQueue.main.async { boxedListener.value.onReadyFromCache(metadata) }
}
}

private func triggerReadyTimedOut() {
guard !isSdkReadyTimedOutFired() else { return }
setSdkReadyTimedOutFired()

getListeners().forEach { listener in
DispatchQueue.main.async { listener.onReadyTimedOut() }
getListeners().forEach { listener in
let boxedListener = UncheckedSendableBox(value: listener)
DispatchQueue.main.async { boxedListener.value.onReadyTimedOut() }
}
}

private func triggerUpdate(_ metadata: SdkUpdateMetadata) {
Logger.d("Triggering SDK event SDK_UPDATE")

getListeners().forEach { listener in
DispatchQueue.main.async { listener.onUpdate(metadata) }
getListeners().forEach { listener in
let boxedListener = UncheckedSendableBox(value: listener)
DispatchQueue.main.async { boxedListener.value.onUpdate(metadata) }
}
}

Expand Down
6 changes: 4 additions & 2 deletions SplitThin/Storage/CoreDataStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -403,15 +403,17 @@ final class CoreDataStorage: @unchecked Sendable {

private func withContext<T>(_ block: @escaping (NSManagedObjectContext) throws -> T) async throws -> T {
let context = container.newBackgroundContext()
return try await withCheckedThrowingContinuation { continuation in

let boxed: UncheckedSendableBox<T> = try await withCheckedThrowingContinuation { continuation in
context.perform {
do {
continuation.resume(returning: try block(context))
continuation.resume(returning: UncheckedSendableBox(value: try block(context)))
} catch {
continuation.resume(throwing: error)
}
}
}
return boxed.value
}

// MARK: - Model Definition
Expand Down
164 changes: 164 additions & 0 deletions release_thin.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#!/bin/bash

# ios-thin-client Release Preparation Script
# Mirrors ios-client/scripts/release.sh.
# Usage: ./release_thin.sh <version>
# Example: ./release_thin.sh 1.0.1-rc1

# Branch name constants - update these if branch naming changes
MASTER_BRANCH="main"
DEVELOPMENT_BRANCH="development"

# Public GitHub repo (canonical for this public SDK; PRs/tags live here).
GITHUB_REPO="splitio/ios-thin-client"

set -e

# Check if version parameter is provided
if [ -z "$1" ]; then
echo "❌ Error: Version parameter is required"
echo "Usage: ./release_thin.sh <version>"
echo "Example: ./release_thin.sh 1.0.1-rc1"
exit 1
fi

VERSION=$1
RELEASE_BRANCH="release/$VERSION"

# Ensure we're in the repo root directory
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"

# Releases must originate on the public GitHub repo (canonical). Harness Code is a
# downstream mirror, so pushing there would never reach GitHub / trigger the tag
# workflow. Refuse to run unless 'origin' points at the GitHub repo.
ORIGIN_URL="$(git config --get remote.origin.url || true)"
case "$ORIGIN_URL" in
*github.com[:/]"$GITHUB_REPO"* )
: ;; # ok, origin is the GitHub repo
* )
echo "❌ Error: 'origin' is not the GitHub repo ($GITHUB_REPO)."
echo " Current origin: ${ORIGIN_URL:-<none>}"
echo " Releases must be run from the GitHub clone (the public repo is canonical;"
echo " Harness Code only mirrors from it). Clone and release from there:"
echo " git clone https://github.com/$GITHUB_REPO.git"
exit 1
;;
esac

# Check if working directory is clean
if [ -n "$(git status --porcelain)" ]; then
echo "❌ Error: Working directory is not clean. Please commit or stash your changes first."
exit 1
fi

# Fetch latest changes from remote
echo "📥 Fetching latest changes from remote..."
git fetch origin

# Get current branch
CURRENT_BRANCH=$(git symbolic-ref --short HEAD)
echo "📑 Current branch: $CURRENT_BRANCH"

# Create release branch from current branch
echo "🌿 Creating branch $RELEASE_BRANCH from $CURRENT_BRANCH..."
git checkout -B "$RELEASE_BRANCH"

# Any version with a "-" suffix (rc, beta, alpha...) is a pre-release
IS_PRERELEASE=false
if [[ "$VERSION" == *-* ]]; then
IS_PRERELEASE=true
fi

# Update Version.swift
echo "📝 Updating Version.swift to $VERSION..."
VERSION_FILE="SplitThin/Common/Version.swift"
if ! grep -q 'private static let version = "[^"]*"' "$VERSION_FILE"; then
echo "❌ Error: could not find version line in $VERSION_FILE"
exit 1
fi
sed -i '' "s/private static let version = \".*\"/private static let version = \"$VERSION\"/" "$VERSION_FILE"

# Update CHANGES.txt if not a pre-release version
if [ "$IS_PRERELEASE" = false ]; then
echo "📝 Updating CHANGES.txt..."

# Prompt for changes
echo ""
echo "Please enter the changes for version $VERSION (one per line)"
echo "Press Enter twice when done (or just press Enter to skip)"
echo ""

CHANGES=""
while true; do
read -r line

# Break on empty line
if [ -z "$line" ]; then
if [ -z "$CHANGES" ]; then
# No changes were entered, just break
break
else
# Confirm if done
read -r -p "Are you done entering changes? (y/n): " confirm
if [[ "$confirm" =~ ^[Yy] ]]; then
break
fi
fi
else
# Add the line to changes
if [ -z "$CHANGES" ]; then
CHANGES="- $line"
else
CHANGES="$CHANGES\n- $line"
fi
fi
done

# Create the new entry
CURRENT_DATE=$(LC_ALL=C date "+%b %-d, %Y")
NEW_ENTRY="$VERSION ($CURRENT_DATE)"
if [ -n "$CHANGES" ]; then
NEW_ENTRY="$NEW_ENTRY\n$CHANGES"
fi

# Insert at the beginning of the file
sed -i '' "1s/^/$NEW_ENTRY\n\n/" CHANGES.txt
fi

# Commit changes
echo "💾 Committing changes..."
if [ "$IS_PRERELEASE" = false ]; then
git add SplitThin/Common/Version.swift CHANGES.txt
git commit -m "chore: Update version to $VERSION and update CHANGES.txt"
else
git add SplitThin/Common/Version.swift
git commit -m "chore: Update version to $VERSION"
fi

# Push changes
echo "📤 Pushing branch to remote..."
git push origin "$RELEASE_BRANCH"

# Determine target branch based on pre-release status
if [ "$IS_PRERELEASE" = true ]; then
TARGET_BRANCH="$DEVELOPMENT_BRANCH"
echo "📊 Pre-release version detected, PR will target the $DEVELOPMENT_BRANCH branch"
else
TARGET_BRANCH="$MASTER_BRANCH"
echo "📊 Regular version detected, PR will target the $MASTER_BRANCH branch"
fi

# Create PR URL on the public GitHub repo (canonical; Harness Code mirrors from it)
PR_URL="https://github.com/$GITHUB_REPO/compare/$TARGET_BRANCH...$RELEASE_BRANCH?expand=1"

echo ""
echo "🎉 Release preparation completed successfully!"
echo ""
echo "Opening browser to create pull request..."
open "$PR_URL" 2>/dev/null || echo "Open this URL to create the PR: $PR_URL"
echo ""
echo "Next steps:"
echo "1. Complete the pull request to merge $RELEASE_BRANCH into $TARGET_BRANCH on GitHub."
echo "2. After merging, the release-tag workflow creates and pushes the tag '$VERSION'."
echo ""