github actions script

This commit is contained in:
Konstantin Wohlwend 2026-01-19 15:53:15 -08:00
parent f9a4a17da8
commit 1034d2e508
13 changed files with 144 additions and 24 deletions

100
.github/workflows/swift-sdk-publish.yaml vendored Normal file
View File

@ -0,0 +1,100 @@
name: Publish Swift SDK to prerelease repo
on:
push:
branches:
- main
paths:
- 'sdks/implementations/swift/**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false # Don't cancel publishing in progress
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout source repo
uses: actions/checkout@v4
with:
path: source
- name: Read version from package.json
id: version
run: |
VERSION=$(jq -r '.version' source/sdks/implementations/swift/package.json)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Swift SDK version: $VERSION"
- name: Check if tag already exists in target repo
id: check-tag
run: |
TAG="v${{ steps.version.outputs.version }}"
echo "Checking if tag $TAG exists in stack-auth/swift-sdk-prerelease..."
# Use the GitHub API to check if the tag exists
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer ${{ secrets.SWIFT_SDK_PUBLISH_TOKEN }}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/stack-auth/swift-sdk-prerelease/git/refs/tags/$TAG")
if [ "$HTTP_STATUS" = "200" ]; then
echo "Tag $TAG already exists, skipping publish"
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "Tag $TAG does not exist, will publish"
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Clone target repo
if: steps.check-tag.outputs.exists == 'false'
run: |
git clone https://x-access-token:${{ secrets.SWIFT_SDK_PUBLISH_TOKEN }}@github.com/stack-auth/swift-sdk-prerelease.git target
- name: Copy Swift SDK to target repo
if: steps.check-tag.outputs.exists == 'false'
run: |
# Remove all files except .git from target
cd target
find . -maxdepth 1 -not -name '.git' -not -name '.' -exec rm -rf {} +
cd ..
# Copy everything from Swift SDK
cp -r source/sdks/implementations/swift/* target/
cp source/sdks/implementations/swift/.gitignore target/ 2>/dev/null || true
# Remove package.json (it's only for turborepo integration, not part of the Swift package)
rm -f target/package.json
- name: Commit and push to target repo
if: steps.check-tag.outputs.exists == 'false'
run: |
cd target
git config user.email "github-actions[bot]@users.noreply.github.com"
git config user.name "github-actions[bot]"
git add -A
# Check if there are changes to commit
if git diff --staged --quiet; then
echo "No changes to commit"
else
git commit -m "Release v${{ steps.version.outputs.version }}"
fi
# Create and push tag
TAG="v${{ steps.version.outputs.version }}"
git tag "$TAG"
git push origin main --tags
echo "Successfully published Swift SDK v${{ steps.version.outputs.version }}"
- name: Summary
run: |
if [ "${{ steps.check-tag.outputs.exists }}" = "true" ]; then
echo "::notice::Skipped publishing - tag v${{ steps.version.outputs.version }} already exists"
else
echo "::notice::Published Swift SDK v${{ steps.version.outputs.version }} to stack-auth/swift-sdk-prerelease"
fi

View File

@ -17,18 +17,18 @@ An interactive iOS application for testing all Stack Auth Swift SDK functions.
open StackAuthiOS.xcodeproj
```
2. Select an iOS Simulator (e.g., "iPhone 17 Pro") as the destination
2. Select an iOS Simulator (e.g., "iPhone 15 Pro" or any available device) as the destination
3. Press ⌘R to build and run
### Option 2: Command Line
```bash
# Build
xcodebuild -scheme StackAuthiOS -destination 'platform=iOS Simulator,name=iPhone 17 Pro' build
# Build (replace device name with an available simulator on your system)
xcodebuild -scheme StackAuthiOS -destination 'platform=iOS Simulator,name=iPhone 15 Pro' build
# Build and run (opens simulator)
xcodebuild -scheme StackAuthiOS -destination 'platform=iOS Simulator,name=iPhone 17 Pro' run
xcodebuild -scheme StackAuthiOS -destination 'platform=iOS Simulator,name=iPhone 15 Pro' run
```
## Features

View File

@ -13,7 +13,7 @@ Add to your `Package.swift`:
```swift
dependencies: [
.package(url: "https://github.com/stack-auth/stack-swift", from: "1.0.0")
.package(url: "https://github.com/stack-auth/swift-sdk-prerelease", from: <version>)
]
```
@ -36,7 +36,7 @@ if let user = try await stack.getUser() {
}
// Sign out
try await user.signOut()
try await stack.signOut()
```
## Design Decisions
@ -126,7 +126,6 @@ Task {
| OAuth | Browser redirect | ASWebAuthenticationSession |
| Redirect methods | Available | Not available (browser-only) |
| React hooks | `useUser()` etc. | Not applicable |
| Error handling | Result types | `throws` |
### Not Available in Swift

View File

@ -35,7 +35,9 @@ actor APIClient {
authenticated: Bool = false,
serverOnly: Bool = false
) async throws -> (Data, HTTPURLResponse) {
let url = URL(string: "\(baseUrl)/api/v1\(path)")!
guard let url = URL(string: "\(baseUrl)/api/v1\(path)") else {
throw StackAuthError(code: "INVALID_URL", message: "Failed to construct request URL from base: \(baseUrl) and path: \(path)")
}
var request = URLRequest(url: url)
request.httpMethod = method
request.cachePolicy = .reloadIgnoringLocalCacheData
@ -119,13 +121,23 @@ actor APIClient {
}
}
// Handle rate limiting
if actualStatus == 429 {
// Handle rate limiting (max 5 retries)
if actualStatus == 429 && attempt < 5 {
if let retryAfter = httpResponse.value(forHTTPHeaderField: "Retry-After"),
let seconds = Double(retryAfter) {
// Use Retry-After header if provided
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
return try await sendWithRetry(request: request, authenticated: authenticated, attempt: attempt + 1)
} else {
// No Retry-After header: use exponential backoff (1s, 2s, 4s, 8s, 16s)
let delayMs = 1000.0 * pow(2.0, Double(attempt))
try await Task.sleep(nanoseconds: UInt64(delayMs * 1_000_000))
}
return try await sendWithRetry(request: request, authenticated: authenticated, attempt: attempt + 1)
}
// Rate limit exhausted after max retries
if actualStatus == 429 {
throw StackAuthError(code: "RATE_LIMITED", message: "Too many requests, please try again later")
}
// Check for known error

View File

@ -336,7 +336,7 @@ public actor CurrentUser {
) async throws -> UserApiKeyFirstView {
var body: [String: Any] = ["description": description]
if let expiresAt = expiresAt {
body["expires_at"] = Int64(expiresAt.timeIntervalSince1970 * 1000)
body["expires_at_millis"] = Int64(expiresAt.timeIntervalSince1970 * 1000)
}
if let scope = scope { body["scope"] = scope }
if let teamId = teamId { body["team_id"] = teamId }

View File

@ -3,9 +3,17 @@ import Foundation
/// A permission granted to a user within a team or project
public struct TeamPermission: Sendable {
public let id: String
public init(id: String) {
self.id = id
}
}
/// A project-level permission
public struct ProjectPermission: Sendable {
public let id: String
public init(id: String) {
self.id = id
}
}

View File

@ -14,13 +14,14 @@ public struct ActiveSession: Sendable {
self.id = json["id"] as? String ?? ""
self.userId = json["user_id"] as? String ?? ""
let createdMillis = json["created_at"] as? Int64 ?? json["created_at_millis"] as? Int64 ?? 0
self.createdAt = Date(timeIntervalSince1970: Double(createdMillis) / 1000.0)
// JSONSerialization returns NSNumber for numeric values, use doubleValue for reliable parsing
let createdMillis = (json["created_at"] as? NSNumber)?.doubleValue ?? 0
self.createdAt = Date(timeIntervalSince1970: createdMillis / 1000.0)
self.isImpersonation = json["is_impersonation"] as? Bool ?? false
if let lastUsedMillis = json["last_used_at"] as? Int64 ?? json["last_used_at_millis"] as? Int64 {
self.lastUsedAt = Date(timeIntervalSince1970: Double(lastUsedMillis) / 1000.0)
if let lastUsedRaw = json["last_used_at"] as? NSNumber {
self.lastUsedAt = Date(timeIntervalSince1970: lastUsedRaw.doubleValue / 1000.0)
} else {
self.lastUsedAt = nil
}

View File

@ -133,7 +133,7 @@ public actor Team {
) async throws -> TeamApiKeyFirstView {
var body: [String: Any] = ["description": description]
if let expiresAt = expiresAt {
body["expires_at"] = Int64(expiresAt.timeIntervalSince1970 * 1000)
body["expires_at_millis"] = Int64(expiresAt.timeIntervalSince1970 * 1000)
}
if let scope = scope { body["scope"] = scope }

View File

@ -25,7 +25,7 @@ struct TestConfig {
do {
let (_, response) = try await URLSession.shared.data(from: url)
if let httpResponse = response as? HTTPURLResponse {
return httpResponse.statusCode < 500
return (200..<300).contains(httpResponse.statusCode)
}
return false
} catch {

View File

@ -215,7 +215,7 @@ Use getAuthHeaders() to generate this header value.
Several sign-in methods may return MultiFactorAuthenticationRequired error when MFA is enabled.
Error format:
code: "multi_factor_authentication_required"
code: "MULTI_FACTOR_AUTHENTICATION_REQUIRED"
message: "Multi-factor authentication is required."
details: { attempt_code: string }

View File

@ -38,7 +38,7 @@ Does not error.
options.description: string?
options.expiresAt: Date | null?
PATCH /api/v1/api-keys/{id} { description, expires_at } [authenticated]
PATCH /api/v1/api-keys/{id} { description, expires_at_millis } [authenticated]
Does not error.

View File

@ -102,7 +102,7 @@ options.scope: string?
Returns: TeamApiKeyFirstView
POST /api/v1/teams/{teamId}/api-keys { description, expires_at, scope } [authenticated]
POST /api/v1/teams/{teamId}/api-keys { description, expires_at_millis, scope } [authenticated]
See types/common/api-keys.spec.md for TeamApiKeyFirstView.
The apiKey property is only returned once at creation time.

View File

@ -5,8 +5,8 @@ The authenticated user with methods to modify their own data.
Extends: User (base-user.spec.md)
Also includes:
- Auth methods (signOut, getAccessToken, etc.)
- Customer methods (payments/customer.spec.md)
- Auth methods (signOut, getAccessToken, etc.)
- Customer methods (payments/customer.spec.md)
## Additional Properties
@ -404,7 +404,7 @@ options.teamId: string? - for team-scoped keys
Returns: UserApiKeyFirstView
POST /api/v1/users/me/api-keys { description, expires_at, scope, team_id } [authenticated]
POST /api/v1/users/me/api-keys { description, expires_at_millis, scope, team_id } [authenticated]
See types/common/api-keys.spec.md for UserApiKeyFirstView.
The apiKey property is only returned once at creation time.