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
100 changes: 100 additions & 0 deletions .github/workflows/swift-sdk-publish.yaml
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +31 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail fast on non-404 API errors during tag check.
Right now 401/403/5xx are treated as “tag missing,” which can mask auth/rate-limit failures and attempt a publish anyway.

🛠️ Suggested fix
-          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
+          if [ "$HTTP_STATUS" = "200" ]; then
+            echo "Tag $TAG already exists, skipping publish"
+            echo "exists=true" >> $GITHUB_OUTPUT
+          elif [ "$HTTP_STATUS" = "404" ]; then
+            echo "Tag $TAG does not exist, will publish"
+            echo "exists=false" >> $GITHUB_OUTPUT
+          else
+            echo "Failed to check tag (HTTP $HTTP_STATUS)"
+            exit 1
+          fi
🤖 Prompt for AI Agents
In @.github/workflows/swift-sdk-publish.yaml around lines 31 - 49, The tag-check
step (id: check-tag) currently treats any non-200 response as "tag missing"
which masks auth/rate-limit/server errors; update the logic that inspects
HTTP_STATUS (for TAG="v${{ steps.version.outputs.version }}") so that: if
HTTP_STATUS == 200 set exists=true, else if HTTP_STATUS == 404 set exists=false,
otherwise log the HTTP status and error context and exit non-zero to fail fast
(do not write exists output) so auth/5xx problems stop the workflow instead of
attempting a publish.


- 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workflow creates version tag without code changes

Medium Severity

The workflow creates and pushes a version tag even when there are no code changes to commit. If someone bumps the version in package.json without modifying any Swift SDK code, the tag check passes (new version doesn't exist), files are copied (with package.json removed), no changes are detected, the commit is skipped, but the tag is still created and pushed. This results in multiple version tags pointing to identical code, violating semantic versioning expectations.

Fix in Cursor Fix in Web


echo "Successfully published Swift SDK v${{ steps.version.outputs.version }}"
Comment on lines +85 to +92
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 }}"
# Create and push tag only after successful commit
TAG="v${{ steps.version.outputs.version }}"
git tag "$TAG"
git push origin main --tags
echo "Successfully published Swift SDK v${{ steps.version.outputs.version }}"
fi

The git push command always executes and pushes tags even when no changes were committed, which could result in orphaned tags or failed pushes to an empty repository state.

View Details

Analysis

Git tag created and pushed even when no commit was made

What fails: In .github/workflows/swift-sdk-publish.yaml, the "Commit and push to target repo" step creates and pushes a git tag even when no changes were staged for commit, resulting in tags being created without corresponding new commits.

How to reproduce:

  1. In a clean target repository clone, if git add -A stages no changes (files already match what should be published), git diff --staged --quiet returns exit code 0
  2. The if condition enters the true branch and echoes "No changes to commit"
  3. However, lines 87-90 (git tag, git push origin main --tags) are outside the if/else block
  4. These commands execute unconditionally, creating a tag without a corresponding commit

Result: Tag is created and pushed pointing to the existing HEAD commit, even though no new commit was made. This can occur when:

  • Version number is incremented in package.json but Swift SDK files haven't actually changed
  • The workflow is triggered by the path filter but copies identical files to the target repo

Expected behavior: Tag creation and push should only occur after a successful commit. According to GitHub Actions bash script best practices, operations that depend on a prior step should be conditional on that step's success.

Fix applied: Moved git tag and git push origin main --tags commands inside the else block so they only execute after a successful commit is created.


- 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
58 changes: 58 additions & 0 deletions apps/e2e/tests/general/sdk-implementations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { exec } from "child_process";
import * as fs from "fs";
import * as path from "path";
import { describe } from "vitest";
import { it } from "../helpers";

// Find all SDK implementations that have a package.json
function findSdkImplementations(): string[] {
const implementationsDir = path.resolve(__dirname, "../../../../sdks/implementations");

if (!fs.existsSync(implementationsDir)) {
return [];
}

const entries = fs.readdirSync(implementationsDir, { withFileTypes: true });
const sdkDirs: string[] = [];

for (const entry of entries) {
if (entry.isDirectory()) {
const packageJsonPath = path.join(implementationsDir, entry.name, "package.json");
if (fs.existsSync(packageJsonPath)) {
sdkDirs.push(entry.name);
}
}
}

return sdkDirs;
}

const sdkImplementations = findSdkImplementations();

describe("SDK implementation tests", () => {
for (const sdk of sdkImplementations) {
describe(`${sdk} SDK`, () => {
it("runs tests successfully", async ({ expect }) => {
const sdkDir = path.resolve(__dirname, `../../../../sdks/implementations/${sdk}`);

const [error, stdout, stderr] = await new Promise<[Error | null, string, string]>((resolve) => {
exec("pnpm run test", { cwd: sdkDir }, (error, stdout, stderr) => {
resolve([error, stdout, stderr]);
});
});

expect(
error,
`Expected ${sdk} SDK tests to pass!\n\n\n\nstdout: ${stdout}\n\n\n\nstderr: ${stderr}`
).toBeNull();
}, 300_000); // 5 minute timeout for SDK tests
});
}

// If no SDKs found, add a placeholder test so the describe block isn't empty
if (sdkImplementations.length === 0) {
it("has no SDK implementations to test", ({ expect }) => {
expect(true).toBe(true);
});
}
});
4 changes: 4 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ packages:
- apps/*
- examples/*
- docs
- sdks/*
- sdks/implementations/*

minimumReleaseAge: 2880
13 changes: 13 additions & 0 deletions sdks/implementations/swift/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
xcuserdata/
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
timeline.xctimeline
playground.xcworkspace
.build/
Carthage/Build/
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
21 changes: 21 additions & 0 deletions sdks/implementations/swift/Examples/StackAuthMacOS/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
name: "StackAuthMacOS",
platforms: [
.macOS(.v14)
],
dependencies: [
.package(name: "StackAuth", path: "../..")
],
targets: [
.executableTarget(
name: "StackAuthMacOS",
dependencies: [
.product(name: "StackAuth", package: "StackAuth")
],
path: "StackAuthMacOS"
)
]
)
111 changes: 111 additions & 0 deletions sdks/implementations/swift/Examples/StackAuthMacOS/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Stack Auth macOS Example

A comprehensive macOS SwiftUI application for testing all Stack Auth SDK functions interactively.

## Prerequisites

- macOS 14.0+
- Swift 5.9+
- A running Stack Auth backend (default: `http://localhost:8102`)

## Running the Example

1. Start the Stack Auth backend:
```bash
cd /path/to/stack-2
pnpm run dev
```

2. Open and run the example:
```bash
cd Examples/StackAuthMacOS
swift run
```

Or open in Xcode:
```bash
open Package.swift
```

## Features

The example app provides a sidebar navigation with the following sections:

### Configuration
- **Settings**: Configure API base URL, project ID, and API keys
- **Logs**: View real-time logs of all SDK operations

### Client App Testing
- **Authentication**
- Sign up with email/password
- Sign in with credentials
- Sign in with wrong password (error testing)
- Sign out
- Get current user
- Get user (or throw)

- **User Management**
- Set display name
- Update client metadata
- Update password
- Get access/refresh tokens
- Get auth headers
- Get partial user from token

- **Teams**
- Create team
- List user's teams
- Get team by ID
- List team members

- **Contact Channels**
- List contact channels

- **OAuth**
- Generate OAuth URLs for Google, GitHub, Microsoft
- Test PKCE code generation

- **Tokens**
- Get access token (JWT format)
- Get refresh token
- Get auth headers
- Test different token stores

### Server App Testing
- **Server Users**
- Create user (basic and with all options)
- List users with pagination
- Get user by ID
- Delete user

- **Server Teams**
- Create team
- List all teams
- Add/remove users from teams
- List team users
- Delete team

- **Sessions**
- Create session (impersonation)
- Use session tokens with client app

## Default Configuration

The example is pre-configured for local development:
- Base URL: `http://localhost:8102`
- Project ID: `internal`
- Publishable Key: `this-publishable-client-key-is-for-local-development-only`
- Secret Key: `this-secret-server-key-is-for-local-development-only`

## SDK Functions Covered

| Category | Functions |
|----------|-----------|
| Auth | signUpWithCredential, signInWithCredential, signOut, getUser, getOAuthUrl |
| User | setDisplayName, update (metadata), updatePassword, getAccessToken, getRefreshToken, getAuthHeaders, getPartialUser |
| Teams | createTeam, listTeams, getTeam, listUsers (team members) |
| Contact | listContactChannels |
| Server Users | createUser, listUsers, getUser, delete, update (metadata, password) |
| Server Teams | createTeam, listTeams, getTeam, addUser, removeUser, listUsers, delete |
| Sessions | createSession |
| Errors | EmailPasswordMismatchError, UserNotSignedInError, PasswordConfirmationMismatchError |
Loading
Loading