mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
github actions script
This commit is contained in:
parent
f9a4a17da8
commit
1034d2e508
100
.github/workflows/swift-sdk-publish.yaml
vendored
Normal file
100
.github/workflows/swift-sdk-publish.yaml
vendored
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 }
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 }
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user