Files
openhuman/.github/workflows/package-android.yml
T
ad9d001261 Feat/e2e appium mac2 (#116)
* Replace Playwright with Appium mac2 + WebDriverIO for E2E testing and add unit tests

- Remove Playwright config and old e2e/ directory
- Add WebDriverIO + Appium mac2 driver for true native macOS app testing
- Add wdio.conf.ts with platform-aware app path detection
- Add scripts/e2e.sh to manage Appium lifecycle under Node 24
- Add E2E smoke, navigation, and Tauri integration specs
- Add tsconfig.e2e.json for E2E test TypeScript config
- Add unit tests for components, store slices, selectors, services, and utils
- Add test infrastructure (setup.ts, MSW handlers, test-utils)
- Update vitest.config.ts with coverage thresholds and setup
- Update package.json scripts and dependencies

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update skills submodule to latest

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add complete E2E login flow test with mock server and onboarding walkthrough

Implements end-to-end testing of the full deep link auth → onboarding → home
flow using Appium mac2, WebDriverIO, and a local HTTP/WebSocket mock server.

Key additions:
- Mock server (port 18473) with all API routes + Socket.IO/Engine.IO WebSocket
  handler to prevent Rust SocketManager crashes
- Deep link helpers to trigger alphahuman:// URLs via macOS `open` command
- Element helpers using XPath selectors (not iOS predicates, which crash on
  mac2 with non-string attributes) and W3C pointer actions for clicking
  WKWebView content (standard element.click() doesn't trigger DOM handlers)
- Login flow spec: 11 tests covering auth deep link, API call verification,
  all 4 onboarding steps, and home page arrival
- Dedicated e2e-build.sh script with cargo clean + mock URL injection
- Cache cleanup and mock URL verification in e2e.sh

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Harden E2E tests and fix config issues from PR review

- e2e.sh: fail fast when dist bundle is missing instead of silently skipping
- setup.ts: add React type import for PersistGate mock annotation
- app-helpers: throw descriptive error on waitForAppReady timeout
- element-helpers: XPath quote escaping via concat() for special chars
- login-flow: fail explicitly when invite code step retry doesn't advance
- smoke.spec: replace no-op assertion with real session ID checks
- vite/vitest configs: remove unused @alphahuman/skill-types alias and
  dead path/fileURLToPath imports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add TypeScript types to E2E element helpers and configure ESLint for E2E files

Remove blanket eslint-disable/ts-nocheck from element-helpers.ts, add explicit
TypeScript types with ChainablePromiseElement import, and add ESLint config block
for test/e2e/ files using tsconfig.e2e.json with WebDriverIO/Mocha globals.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix CI submodule checkout by adding XGH_TOKEN_READ secret

The skills submodule is a private repo that requires authentication.
The build and package-android workflows were missing the token that
package-and-publish already had.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix CI: install skills deps, format with Prettier, lower coverage thresholds

- build.yml: add skills dependency install step and remove redundant
  frontend build (tauri build already runs build:app via beforeBuildCommand)
- Run Prettier --write on all 32 files with formatting drift
- Lower vitest coverage thresholds to match actual coverage (~20%)
  until test coverage improves organically

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove stray globals.d.ts that failed Prettier check

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix ESLint parsing for wdio.conf.ts and type remaining E2E helpers

- Add wdio.conf.ts to ESLint E2E config block so TS parser covers it
- Remove blanket eslint-disable/ts-nocheck from deep-link-helpers.ts
  and app-helpers.ts, add explicit TypeScript types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix unnecessary regex escape in useSettingsNavigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Ghost Scripter <ghostscripter@zerolend.xyz>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 17:10:06 +05:30

267 lines
10 KiB
YAML

# Separate workflow for building, packaging, and publishing the Android APK.
# Runs independently from the desktop package-and-publish workflow.
name: Package Android
on:
workflow_dispatch:
inputs:
publish:
description: 'Publish to release (requires main branch)'
type: boolean
default: false
workflow_run:
workflows: ['Version Bump']
types:
- completed
branches:
- main
pull_request:
branches:
- main
env:
IS_PR: ${{ github.event_name == 'pull_request' }}
SHOULD_PUBLISH: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && (github.event_name != 'workflow_dispatch' || github.event.inputs.publish == 'true') }}
PUBLISH_REPO: alphahumanxyz/alphahuman
XGH_TOKEN: ${{ secrets.XGH_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
get-version:
runs-on: ubuntu-latest
outputs:
package-version: ${{ steps.extract-version.outputs.package-version }}
tag-name: ${{ steps.extract-version.outputs.tag-name }}
should-publish: ${{ steps.extract-version.outputs.should-publish }}
release-name: ${{ steps.extract-version.outputs.release-name }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
- name: Extract version and tag
id: extract-version
run: |
PACKAGE_VERSION=$(node -p "require('./package.json').version")
TAG_NAME="v${PACKAGE_VERSION}"
RELEASE_NAME="AlphaHuman v${PACKAGE_VERSION}"
echo "package-version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT
echo "tag-name=$TAG_NAME" >> $GITHUB_OUTPUT
echo "release-name=$RELEASE_NAME" >> $GITHUB_OUTPUT
echo "should-publish=$SHOULD_PUBLISH" >> $GITHUB_OUTPUT
echo "Extracted version: $PACKAGE_VERSION"
echo "Generated tag: $TAG_NAME"
check-release:
runs-on: ubuntu-latest
needs: get-version
outputs:
release-id: ${{ steps.find-release.outputs.release-id }}
should-skip: ${{ steps.find-release.outputs.should-skip }}
steps:
- name: Find existing release
id: find-release
env:
TAG_NAME: ${{ needs.get-version.outputs.tag-name }}
run: |
if [ "$SHOULD_PUBLISH" != "true" ]; then
echo "Publishing disabled (non-main branch or PR)"
echo "should-skip=false" >> $GITHUB_OUTPUT
echo "release-id=" >> $GITHUB_OUTPUT
exit 0
fi
echo "Looking for release with tag: $TAG_NAME"
RESPONSE=$(curl -s -w "\n%{http_code}" -H "Authorization: Bearer $XGH_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/$PUBLISH_REPO/releases/tags/$TAG_NAME")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" = "200" ]; then
RELEASE_ID=$(echo "$BODY" | jq -r '.id')
IS_DRAFT=$(echo "$BODY" | jq -r '.draft')
# Check if Android AAB is already uploaded
AAB_EXISTS=$(echo "$BODY" | jq -r '.assets[]? | select(.name | endswith(".aab")) | .name' | head -1)
if [ -n "$AAB_EXISTS" ]; then
echo "Android AAB already exists in release: $AAB_EXISTS"
echo "should-skip=true" >> $GITHUB_OUTPUT
echo "release-id=$RELEASE_ID" >> $GITHUB_OUTPUT
else
echo "Release found (ID: $RELEASE_ID, draft: $IS_DRAFT), no AAB yet"
echo "should-skip=false" >> $GITHUB_OUTPUT
echo "release-id=$RELEASE_ID" >> $GITHUB_OUTPUT
fi
elif [ "$HTTP_CODE" = "404" ]; then
echo "No release found for $TAG_NAME — will build artifact only (no publish)"
echo "should-skip=false" >> $GITHUB_OUTPUT
echo "release-id=" >> $GITHUB_OUTPUT
else
echo "Warning: Failed to check release (HTTP $HTTP_CODE)"
echo "should-skip=false" >> $GITHUB_OUTPUT
echo "release-id=" >> $GITHUB_OUTPUT
fi
build-android:
name: Build and package Android AAB
needs: [get-version, check-release]
if: ${{ !failure() && !cancelled() && needs.check-release.outputs.should-skip != 'true' }}
environment: ${{ github.ref == 'refs/heads/main' && 'Production' || '' }}
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
submodules: true
token: ${{ secrets.XGH_TOKEN_READ }}
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}
- name: Setup Node.js 24.x
uses: actions/setup-node@v4
with:
node-version: 24.x
cache: 'yarn'
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
- name: Setup Java 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Install Android NDK
run: sdkmanager --install "ndk;27.0.12077973"
- name: Set NDK path
run: echo "NDK_HOME=$ANDROID_HOME/ndk/27.0.12077973" >> $GITHUB_ENV
- name: Cache Cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-android-cargo-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-android-cargo-
- name: Cache node modules
id: yarn-cache
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-build-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-build-
- name: Install dependencies
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
- name: Cache skills node modules
id: skills-yarn-cache
uses: actions/cache@v4
with:
path: skills/node_modules
key: ${{ runner.os }}-skills-${{ hashFiles('skills/yarn.lock') }}
restore-keys: |
${{ runner.os }}-skills-
- name: Install skills dependencies
if: steps.skills-yarn-cache.outputs.cache-hit != 'true'
run: cd skills && yarn install --frozen-lockfile
- name: Build skills
run: cd skills && yarn build
- name: Build frontend
run: yarn build
env:
NODE_ENV: production
VITE_BACKEND_URL: ${{ vars.VITE_BACKEND_URL }}
VITE_TELEGRAM_BOT_USERNAME: ${{ secrets.VITE_TELEGRAM_BOT_USERNAME }}
VITE_TELEGRAM_BOT_ID: ${{ secrets.VITE_TELEGRAM_BOT_ID }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
VITE_SKILLS_GITHUB_REPO: ${{ vars.VITE_SKILLS_GITHUB_REPO }}
VITE_DEBUG: ${{ vars.VITE_DEBUG }}
- name: Initialize Tauri Android
run: npx tauri android init
- name: Decode keystore
if: needs.get-version.outputs.should-publish != ''
env:
ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }}
run: |
echo "$ANDROID_KEYSTORE" | base64 --decode > ${{ github.workspace }}/release.jks
- name: Build Android AAB (signed)
if: needs.get-version.outputs.should-publish != ''
env:
BASE_URL: ${{ vars.BASE_URL }}
ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/release.jks
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: npx tauri android build --aab true
- name: Build Android AAB (unsigned)
if: needs.get-version.outputs.should-publish == ''
env:
BASE_URL: ${{ vars.BASE_URL }}
run: npx tauri android build --aab true
- name: Find AAB
id: find-aab
run: |
AAB_ROOT="src-tauri/gen/android"
echo "Searching for AABs in $AAB_ROOT..."
find "$AAB_ROOT" -name '*.aab' -type f | sort
AAB_PATH=$(find "$AAB_ROOT" -name '*.aab' -type f | head -1)
if [ -z "$AAB_PATH" ]; then
echo "No AAB found"
exit 1
fi
AAB_FILENAME=$(basename "$AAB_PATH")
echo "Selected AAB: $AAB_PATH"
echo "path=$AAB_PATH" >> $GITHUB_OUTPUT
echo "filename=$AAB_FILENAME" >> $GITHUB_OUTPUT
- name: Upload AAB artifact
uses: actions/upload-artifact@v4
with:
name: android-aab
path: ${{ steps.find-aab.outputs.path }}
- name: Upload AAB to release
if: needs.get-version.outputs.should-publish == 'true' && needs.check-release.outputs.release-id != ''
env:
RELEASE_ID: ${{ needs.check-release.outputs.release-id }}
run: |
curl -X POST -H "Authorization: Bearer $XGH_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary "@${{ steps.find-aab.outputs.path }}" \
"https://uploads.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID/assets?name=${{ steps.find-aab.outputs.filename }}"
# Requires secret GOOGLE_PLAY_SERVICE_ACCOUNT_JSON and vars.GOOGLE_PLAY_UPLOAD_ENABLED = 'true'
# Track: internal | alpha | beta | production (use vars.GOOGLE_PLAY_TRACK to override)
- name: Upload AAB to Google Play
if: needs.get-version.outputs.should-publish == 'true' && vars.GOOGLE_PLAY_UPLOAD_ENABLED == 'true'
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
packageName: com.alphahuman.app
releaseFiles: ${{ steps.find-aab.outputs.path }}
track: ${{ vars.GOOGLE_PLAY_TRACK || 'internal' }}
releaseName: AlphaHuman v${{ needs.get-version.outputs.package-version }}
status: completed