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
21 changes: 21 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
self-hosted-runner:
# Labels of self-hosted runner in array of strings.
labels:
- macOS
- packrat-e2e

# Configuration variables in array of strings defined in your repository or
# organization. `null` means disabling configuration variables check.
# Empty array means no configuration variable is allowed.
config-variables: null

# Configuration for file paths. The keys are glob patterns to match to file
# paths relative to the repository root. The values are the configurations for
# the file paths. Note that the path separator is always '/'.
# The following configurations are available.
#
# "ignore" is an array of regular expression patterns. Matched error messages
# are ignored. This is similar to the "-ignore" command line option.
paths:
# .github/workflows/**/*.yml:
# ignore: []
246 changes: 246 additions & 0 deletions .github/workflows/swift-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
name: Swift E2E Tests

on:
pull_request:
branches: ["**"]
paths:
- "apps/swift/**"
- "packages/api/src/**"
- "packages/api/drizzle/**"
- "packages/api/package.json"
- "package.json"
- "bun.lock"
- ".github/workflows/swift-e2e.yml"
push:
branches: [main, development]
paths:
- "apps/swift/**"
- "packages/api/src/**"
- "packages/api/drizzle/**"
- "packages/api/package.json"
- "package.json"
- "bun.lock"
- ".github/workflows/swift-e2e.yml"
schedule:
- cron: "17 8 * * *"
workflow_dispatch:
inputs:
run_macos_ui:
description: "Run the full macOS UI suite on a self-hosted Mac runner"
required: false
type: boolean
default: true
run_ios_ui:
description: "Run the exploratory Swift iOS UI suite on a GitHub-hosted macOS runner"
required: false
type: boolean
default: false

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

env:
XCODE_VERSION: "26.2"
E2E_API_BASE_URL: ${{ secrets.SWIFT_E2E_API_BASE_URL }}
E2E_EMAIL: ${{ secrets.E2E_TEST_EMAIL }}
E2E_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }}
E2E_SCREENSHOT_DIR: ${{ github.workspace }}/apps/swift/TestResults/screenshots
PACKRAT_NATIVEWIND_UI_GITHUB_TOKEN: ${{ secrets.PACKRAT_NATIVEWIND_UI_GITHUB_TOKEN }}

jobs:
macos-ui:
name: macOS Swift UI E2E
runs-on: [self-hosted, macOS, packrat-e2e]
timeout-minutes: 45
if: >
github.event_name == 'schedule' ||
github.event_name == 'push' ||
(github.event_name == 'workflow_dispatch' && inputs.run_macos_ui) ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)

steps:
- name: Checkout repository
uses: actions/checkout@v6

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/swift-e2e.yml

Repository: PackRat-AI/PackRat

Length of output: 9597


Pin all third-party actions to immutable commit SHAs.

Actions currently use mutable tags (@v*), which allows silent updates that can introduce supply-chain attacks. Replace with full commit SHAs across:

  • actions/checkout@v6 (lines 67, 167)
  • maxim-lobanov/setup-xcode@v1 (lines 70, 170)
  • oven-sh/setup-bun@v2 (lines 75, 175)
  • actions/upload-artifact@v7 (lines 133, 141, 150, 224, 232, 241)
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 66-67: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 67-67: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/swift-e2e.yml at line 67, In the file
.github/workflows/swift-e2e.yml, replace all mutable version tags with full
commit SHAs to prevent supply-chain attacks. Specifically: at lines 67 and 167,
replace actions/checkout@v6 with the v6 release commit SHA; at lines 70 and 170,
replace maxim-lobanov/setup-xcode@v1 with its commit SHA; at lines 75 and 175,
replace oven-sh/setup-bun@v2 with its commit SHA; and at lines 133, 141, 150,
224, 232, and 241, replace actions/upload-artifact@v7 with its commit SHA. Use
the official GitHub release pages for each action to find the correct commit
SHAs corresponding to each version tag.

Sources: Coding guidelines, Linters/SAST tools


Comment on lines +66 to +68

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check the workflow file and examine the specified lines
fd 'swift-e2e.yml' --type f

Repository: PackRat-AI/PackRat

Length of output: 44


🏁 Script executed:

# Read the workflow file to see lines 66-68 and 166-168
cat -n .github/workflows/swift-e2e.yml | sed -n '60,75p; 160,175p'

Repository: PackRat-AI/PackRat

Length of output: 1246


🌐 Web query:

actions/checkout persist-credentials false security best practice

💡 Result:

Setting persist-credentials: false in actions/checkout is a recommended security best practice to prevent the automatic persistence of Git credentials (such as the GITHUB_TOKEN or SSH keys) into the runner's local Git configuration [1][2]. By default (persist-credentials: true), the action configures the runner's local Git environment with the necessary authentication credentials to allow subsequent steps to perform authenticated Git operations (like git push or git fetch) automatically [3][4]. However, this also makes these credentials accessible to any subsequent code or action running within the same job [5]. Key points regarding this practice: Security Benefit: Disabling this feature reduces the risk of credential exposure [1]. If a malicious or vulnerable action is executed in your workflow, it cannot easily read the stored credentials from the Git configuration files [2][5]. When to use false: You should set persist-credentials: false in all workflows that do not explicitly require the workflow to perform authenticated Git operations [1][2]. This is the most secure configuration and limits the attack surface of your CI/CD pipeline [2]. When to keep true: You should only leave it as the default (true) if your workflow specifically needs to perform authenticated Git commands, such as pushing commits, creating tags, or fetching from private repositories using the automatically provided credentials [3][4]. Context for Modern Versions: Recent versions of actions/checkout have improved how credentials are handled (e.g., storing them in temporary locations rather than directly in .git/config) to mitigate some risks [3][6][7]. Even with these improvements, explicitly setting persist-credentials: false remains a best practice to follow the principle of least privilege [1][2].

Citations:


Pin actions/checkout to commit SHA and disable credential persistence in both jobs.

actions/checkout@v6 uses a mutable tag instead of a full commit SHA (required by your coding guidelines to prevent supply-chain attacks). Additionally, set persist-credentials: false to reduce credential exposure—this workflow only needs checkout and doesn't require authenticated git operations.

Suggested patch
      - name: Checkout repository
        uses: actions/checkout@<full_commit_sha>
+       with:
+         persist-credentials: false

Also applies to: 166-168

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 66-67: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 67-67: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/swift-e2e.yml around lines 66 - 68, The workflow uses the
mutable tag actions/checkout@v6 instead of a pinned commit SHA, which violates
security guidelines. At both occurrences of the actions/checkout step (at lines
66-68 and 166-168 in the swift-e2e.yml file), replace the version tag with a
specific commit SHA, and add the persist-credentials option set to false. This
ensures supply-chain attack prevention by pinning to an immutable commit, and
reduces credential exposure since this workflow only needs checkout
functionality without authenticated git operations.

Source: Linters/SAST tools

- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ env.XCODE_VERSION }}

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
cache: true
Comment on lines +75 to +78

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if the workflow file exists and read the relevant lines
if [ -f ".github/workflows/swift-e2e.yml" ]; then
  echo "=== Lines 75-78 ==="
  sed -n '75,78p' ".github/workflows/swift-e2e.yml"
  echo ""
  echo "=== Lines 175-178 ==="
  sed -n '175,178p' ".github/workflows/swift-e2e.yml"
else
  echo "File not found: .github/workflows/swift-e2e.yml"
fi

Repository: PackRat-AI/PackRat

Length of output: 307


🏁 Script executed:

# Search for the declared Bun version in common config files
echo "=== Searching for bun version declarations ==="
rg "bun.*version|bun@" --type json --type yaml --type toml --type md -i

Repository: PackRat-AI/PackRat

Length of output: 989


🏁 Script executed:

# Check for all bun-version references in the workflow file
echo "=== All bun-version references in swift-e2e.yml ==="
rg "bun-version" ".github/workflows/swift-e2e.yml" -n

Repository: PackRat-AI/PackRat

Length of output: 181


Pin Bun version in CI instead of latest. Using bun-version: latest makes runs non-reproducible and can break unexpectedly. Pin to the repo's declared toolchain version (bun@1.3.10 from package.json) instead.

Both instances at lines 77 and 177 need to be updated.

Suggested patch
-          bun-version: latest
+          bun-version: 1.3.10
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
cache: true
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
cache: true
🧰 Tools
🪛 zizmor (1.25.2)

[error] 75-75: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/swift-e2e.yml around lines 75 - 78, Replace the dynamic
`bun-version: latest` with a pinned version in the setup-bun action
configuration. Change the bun-version parameter to use the specific version
declared in package.json (1.3.10) instead of latest. This change must be applied
at both occurrences where the setup-bun action is used in the workflow to ensure
reproducible CI runs.


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

- name: Verify Swift E2E secrets
run: |
missing=()
[ -z "${E2E_EMAIL:-}" ] && missing+=("E2E_TEST_EMAIL")
[ -z "${E2E_PASSWORD:-}" ] && missing+=("E2E_TEST_PASSWORD")
[ -z "${E2E_API_BASE_URL:-}" ] && missing+=("SWIFT_E2E_API_BASE_URL")
[ -z "${NEON_DATABASE_URL:-}" ] && missing+=("NEON_DEV_DATABASE_URL")
if [ ${#missing[@]} -gt 0 ]; then
echo "::error::Required Swift E2E secrets missing: ${missing[*]}"
exit 1
fi
env:
NEON_DATABASE_URL: ${{ secrets.NEON_DEV_DATABASE_URL }}

- name: Check Automation Mode status
run: |
automationmodetool status || true

- name: Generate Swift Xcode project
run: bun run swift

- name: Seed E2E test user
run: bun run --filter @packrat/api db:seed:e2e-user
env:
NEON_DATABASE_URL: ${{ secrets.NEON_DEV_DATABASE_URL }}
E2E_TEST_EMAIL: ${{ env.E2E_EMAIL }}
E2E_TEST_PASSWORD: ${{ env.E2E_PASSWORD }}

- name: Run macOS Swift UI E2E
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
caffeinate -dimsu bun run e2e:swift:mac-smoke
else
caffeinate -dimsu bun run e2e:swift:mac-ui
fi

- name: Summarize macOS xcresult
if: always()
run: |
result="$(find apps/swift/TestResults -maxdepth 1 -name '*.xcresult' -type d | sort | tail -1)"
if [ -z "$result" ]; then
echo "No xcresult bundle found."
exit 0
fi
echo "### macOS Swift UI E2E" >> "$GITHUB_STEP_SUMMARY"
echo "\`$result\`" >> "$GITHUB_STEP_SUMMARY"
xcrun xcresulttool get test-results summary --path "$result" | tee -a "$GITHUB_STEP_SUMMARY"

- name: Upload macOS xcresult
if: always()
uses: actions/upload-artifact@v7
with:
name: swift-macos-ui-xcresult
path: apps/swift/TestResults/*.xcresult
retention-days: 14

- name: Upload macOS screenshots
if: always()
uses: actions/upload-artifact@v7
with:
name: swift-macos-ui-screenshots
path: apps/swift/TestResults/screenshots/
if-no-files-found: ignore
retention-days: 14

- name: Upload macOS failure triage bundle
if: failure()
uses: actions/upload-artifact@v7
with:
name: swift-macos-ui-failure-triage
path: apps/swift/TestResults/
if-no-files-found: ignore
retention-days: 14

ios-ui:
name: iOS Swift UI E2E (Exploratory)
runs-on: macos-15
timeout-minutes: 60
if: >
github.event_name == 'schedule' ||
(github.event_name == 'workflow_dispatch' && inputs.run_ios_ui)

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ env.XCODE_VERSION }}

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
cache: true

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

- name: Verify Swift E2E secrets
run: |
missing=()
[ -z "${E2E_EMAIL:-}" ] && missing+=("E2E_TEST_EMAIL")
[ -z "${E2E_PASSWORD:-}" ] && missing+=("E2E_TEST_PASSWORD")
[ -z "${E2E_API_BASE_URL:-}" ] && missing+=("SWIFT_E2E_API_BASE_URL")
[ -z "${NEON_DATABASE_URL:-}" ] && missing+=("NEON_DEV_DATABASE_URL")
if [ ${#missing[@]} -gt 0 ]; then
echo "::error::Required Swift E2E secrets missing: ${missing[*]}"
exit 1
fi
env:
NEON_DATABASE_URL: ${{ secrets.NEON_DEV_DATABASE_URL }}

- name: Generate Swift Xcode project
run: bun run swift

- name: Seed E2E test user
run: bun run --filter @packrat/api db:seed:e2e-user
env:
NEON_DATABASE_URL: ${{ secrets.NEON_DEV_DATABASE_URL }}
E2E_TEST_EMAIL: ${{ env.E2E_EMAIL }}
E2E_TEST_PASSWORD: ${{ env.E2E_PASSWORD }}

- name: Run iOS Swift UI E2E
run: bun run e2e:swift:ios

- name: Summarize iOS xcresult
if: always()
run: |
result="$(find apps/swift/TestResults -maxdepth 1 -name '*.xcresult' -type d | sort | tail -1)"
if [ -z "$result" ]; then
echo "No xcresult bundle found."
exit 0
fi
echo "### iOS Swift UI E2E" >> "$GITHUB_STEP_SUMMARY"
echo "\`$result\`" >> "$GITHUB_STEP_SUMMARY"
xcrun xcresulttool get test-results summary --path "$result" | tee -a "$GITHUB_STEP_SUMMARY"

- name: Upload iOS xcresult
if: always()
uses: actions/upload-artifact@v7
with:
name: swift-ios-ui-xcresult
path: apps/swift/TestResults/*.xcresult
retention-days: 14

- name: Upload iOS screenshots
if: always()
uses: actions/upload-artifact@v7
with:
name: swift-ios-ui-screenshots
path: apps/swift/TestResults/screenshots/
if-no-files-found: ignore
retention-days: 14

- name: Upload iOS failure triage bundle
if: failure()
uses: actions/upload-artifact@v7
with:
name: swift-ios-ui-failure-triage
path: apps/swift/TestResults/
if-no-files-found: ignore
retention-days: 14
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ apps/swift/xcconfig/*.local.xcconfig
apps/swift/PackRat.xcodeproj/
apps/swift/*.xcworkspace/xcuserdata/
apps/swift/DerivedData/
apps/swift/TestResults/
apps/swift/.build/
apps/swift/.swiftpm/
apps/swift/Package.resolved
Expand Down
105 changes: 105 additions & 0 deletions apps/swift/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# PackRat Swift Testing

The generated Xcode project is not committed. Regenerate it after changing
`project.yml`:

```sh
bun swift
```

If Xcode or SwiftPM reports a temporary-directory error on this machine, ensure
the configured temp directory exists:

```sh
mkdir -p /Volumes/CrucialX10/tmp/andrewbierman
```

## Commands

```sh
bun run test:swift:runner
bun run test:swift:unit
bun run e2e:swift:ios-smoke
bun run e2e:swift:ios
bun run e2e:swift:mac
bun run e2e:swift:mac-smoke
bun run e2e:swift:mac-ui
```

`e2e:swift` defaults to iOS UI tests for compatibility with the original
runner. All Xcode result bundles are written under `apps/swift/TestResults/`.

Smoke modes are intentionally small PR gates:

- `e2e:swift:mac-smoke`: macOS login, sidebar navigation, and pack create/add-item.
- `e2e:swift:ios-smoke`: iOS login, tab navigation, and pack create.

Full modes are the platform confidence gates:

- `e2e:swift:mac-ui`: full native macOS app UI suite.
- `e2e:swift:ios`: exploratory native Swift iOS app UI suite. This is separate
from the existing Expo iOS app, which remains covered by Maestro.

UI modes require credentials in the process environment or `.env.local`:

```sh
E2E_EMAIL=...
E2E_PASSWORD=...
```

The runner also accepts `E2E_TEST_EMAIL` and `E2E_TEST_PASSWORD`, then forwards
them to XCTest as `E2E_EMAIL` and `E2E_PASSWORD`. Credential values are not
printed by the runner.

Set `E2E_API_BASE_URL` to point UI tests at a specific API worker without
changing the app's saved preferences:

```sh
E2E_API_BASE_URL=http://localhost:8788
```

## CI

Swift E2E CI is defined in `.github/workflows/swift-e2e.yml`.

- Pull requests run the macOS smoke subset on a self-hosted Mac runner.
- Pushes, scheduled runs, and manual macOS runs execute the full macOS suite.
- Swift iOS runs nightly or manually and is labeled exploratory while the Expo
app remains the production iOS app.
- Each CI run uploads `.xcresult` bundles, screenshots, failure triage artifacts,
and a GitHub step summary generated with `xcresulttool`.

See `docs/ci/swift-e2e-runner.md` for self-hosted Mac runner setup.

## Data Isolation

Swift E2E tests use unique names for records they create. That keeps repeated
runs safe against shared account state, but it does not fully clean historical
test data from the backend. If the shared E2E account starts accumulating enough
data to affect performance or assertions, add API-backed cleanup helpers or a
test-only reset endpoint and call it from the runner before/after UI modes.

## Signing

`e2e:swift:mac` passes `CODE_SIGNING_ALLOWED=NO` so the local compile gate can
run without provisioning.

`e2e:swift:mac-ui` must still be signed because XCTest launches a runner app,
but the runner uses Xcode's local ad-hoc identity (`Sign to Run Locally`) so
smoke tests do not block on private-key prompts.

Normal signed builds use automatic signing with team `666HGMV2LU`. If command-
line signing fails with `errSecInternalComponent`, the certificate is installed
but `codesign` cannot access the private key from the login keychain. Unlock the
keychain and allow Apple tooling to use the key before rerunning:

```sh
security unlock-keychain ~/Library/Keychains/login.keychain-db
security set-key-partition-list -S apple-tool:,apple: -s ~/Library/Keychains/login.keychain-db
```

## Worktree Hygiene

The Swift branch is active and may move while multiple agents are working.
Fetch before editing shared Swift files, then compare against
`origin/claude/swift-mac-app-effort-tTGd7` before final verification.
2 changes: 2 additions & 0 deletions apps/swift/Sources/PackRat/Features/Catalog/CatalogView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ struct CatalogView: View {
TextField("Search tents, packs, sleeping bags…", text: $bvm.searchText)
.onChange(of: vm.searchText) { vm.onSearchTextChanged() }
.onSubmit { Task { await vm.search(reset: true) } }
.accessibilityIdentifier("catalog_search")
if vm.isLoading {
ProgressView().controlSize(.small)
} else if !vm.searchText.isEmpty {
Button { vm.searchText = "" } label: {
Image(systemName: "xmark.circle.fill").foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.accessibilityIdentifier("catalog_search_clear")
}
}
.padding(10)
Expand Down
Loading
Loading