Skip to content
Merged
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
32 changes: 32 additions & 0 deletions .github/tc-context/feature-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Feature context for Test Companion exploration

Edit this file in the PR that ships the feature. The next workflow (exploration) reads it
and passes it to `tc app generate` as the exploration instructions.

## App
BStack Bank (Android, package com.yash.bankingapp)

## Feature under test
Region functionality. From the dashboard, a logged-in user opens the Region quick action and
the app shows the user's region details (Region Info screen with a "Region Details" section),
fetched live from the device's public IP address.

## Where to find it in the app
1. Launch the app. On the login screen, tap the "Regular User" button to log in as a regular user.
2. On the dashboard, find the Region option in the Quick Actions. Its label shows the detected
city name, or "Region" while the location is not known yet.
3. Tap it to open the "Region Info" screen.

(Login credentials come from your test data, never put secrets here.)

## What to verify
- Region quick action is visible on the dashboard after logging in as a Regular User.
- Tapping it opens the Region Info screen.
- Region details are fetched and shown, including the "Region / State" row, under "Region Details".
- The refresh button reloads the details.
- If the region data cannot be fetched, the screen shows "Could not fetch region data".

## What good test cases look like
- One user-visible behaviour per test case
- Numbered steps, each with one clear expected result
- Include happy path and key negative cases (for example no network, refresh, back navigation)
38 changes: 38 additions & 0 deletions .github/workflows/build-apk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,81 +11,119 @@

jobs:
build:
outputs:
bs_url: ${{ steps.appautomate.outputs.bs_url }}
custom_id: ${{ steps.appautomate.outputs.custom_id }}
apk_name: ${{ steps.apk.outputs.name }}
# Only when: PR was MERGED, from branch pr/workflow, and title starts with "feat(dev): "
if: >-
github.event.pull_request.merged == true &&
github.event.pull_request.head.ref == 'pr/workflow' &&
startsWith(github.event.pull_request.title, 'feat(dev): ')
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}

# Ubuntu runners already include the Android SDK; pin JDK 17 (cached, fast).
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Install JS dependencies
run: npm ci

- name: Bump app version to 2.0.1
run: |
node -e "
const fs=require('fs');
const j=JSON.parse(fs.readFileSync('app.json','utf8'));
j.expo.version='2.0.1';
fs.writeFileSync('app.json',JSON.stringify(j,null,2)+'\n');
console.log('version ->', j.expo.version);"

- name: Create .env from secrets
env:
EXPO_PUBLIC_STRIPE_PK: ${{ secrets.EXPO_PUBLIC_STRIPE_PK }}
EXPO_PUBLIC_API_URL: ${{ secrets.EXPO_PUBLIC_API_URL }}
EXPO_PUBLIC_CLOUDINARY_CLOUD_NAME: ${{ secrets.EXPO_PUBLIC_CLOUDINARY_CLOUD_NAME }}
EXPO_PUBLIC_CLOUDINARY_UPLOAD_PRESET: ${{ secrets.EXPO_PUBLIC_CLOUDINARY_UPLOAD_PRESET }}
run: |
{
echo "EXPO_PUBLIC_STRIPE_PK=$EXPO_PUBLIC_STRIPE_PK"
echo "EXPO_PUBLIC_API_URL=$EXPO_PUBLIC_API_URL"
echo "EXPO_PUBLIC_CLOUDINARY_CLOUD_NAME=$EXPO_PUBLIC_CLOUDINARY_CLOUD_NAME"
echo "EXPO_PUBLIC_CLOUDINARY_UPLOAD_PRESET=$EXPO_PUBLIC_CLOUDINARY_UPLOAD_PRESET"
} > .env

# /android is git-ignored (generated), so regenerate it.
- name: Expo prebuild (android)
run: npx expo prebuild --platform android --no-install

- uses: gradle/actions/setup-gradle@v4

- name: Build release APK
working-directory: android
run: set -a && source ../.env && set +a && ./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a --no-daemon

- name: Rename APK
id: apk
run: |
SRC=$(ls android/app/build/outputs/apk/release/*.apk | head -1)
NAME="BStackBank-2.0.1-${{ github.event.pull_request.number }}-$(date +%Y%m%d-%H%M).apk"
cp "$SRC" "$NAME"
echo "name=$NAME" >> "$GITHUB_OUTPUT"

# Stable custom id: re-uploads keep the same name, so later flows can always find the latest build.
- name: Upload APK to BrowserStack App Automate
id: appautomate
env:
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
run: node scripts/upload-to-appautomate.js "${{ steps.apk.outputs.name }}" "BStackBankLatest"

- name: Upload APK to Google Drive
env:
GDRIVE_SERVICE_ACCOUNT_JSON: ${{ secrets.GDRIVE_SERVICE_ACCOUNT_JSON }}
GDRIVE_FOLDER_ID: ${{ secrets.GDRIVE_FOLDER_ID }}
run: node scripts/upload-to-drive.js "${{ steps.apk.outputs.name }}"

# Handoff for the exploration flow: BS app id + feature context from the repo.
- name: Write handoff file
env:
BS_URL: ${{ steps.appautomate.outputs.bs_url }}
CUSTOM_ID: ${{ steps.appautomate.outputs.custom_id }}
APK_NAME: ${{ steps.apk.outputs.name }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
run: |
mkdir -p handoff
cp .github/tc-context/feature-context.md handoff/feature-context.md
node -e '
const fs=require("fs"), e=process.env;
fs.writeFileSync("handoff/handoff.json", JSON.stringify({
bs_url:e.BS_URL, custom_id:e.CUSTOM_ID, apk_name:e.APK_NAME,
pr_number:e.PR_NUMBER, pr_title:e.PR_TITLE, sha:e.MERGE_SHA
},null,2)+"\n");'
cat handoff/handoff.json

- uses: actions/upload-artifact@v4
with:
name: tc-handoff
path: handoff/
retention-days: 7

- uses: actions/upload-artifact@v4
with:
name: ${{ steps.apk.outputs.name }}
path: ${{ steps.apk.outputs.name }}
retention-days: 7

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,5 @@ yash-demo-banking-app-firebase-adminsdk-fbsvc-a1594354a1.json
# Firebase / Google config (contains API keys — inject at build time via EAS Secrets)
# google-services.json
GoogleService-Info.plist

plan.md
47 changes: 47 additions & 0 deletions scripts/upload-to-appautomate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env node
// Uploads an APK to BrowserStack App Automate with a stable custom_id and prints the result.
// Usage: node scripts/upload-to-appautomate.js <apk> <customId>
// Env: BROWSERSTACK_USERNAME, BROWSERSTACK_ACCESS_KEY
// Writes GitHub Actions outputs (bs_url, custom_id, shareable_id) when GITHUB_OUTPUT is set.
// No dependencies: Node 18+ (fetch, FormData, Blob).
const fs = require('fs');
const { Buffer } = require('buffer');

async function main() {
const [file, customId] = process.argv.slice(2);
if (!file || !fs.existsSync(file)) throw new Error(`APK not found: ${file}`);
if (!customId || !/^[A-Za-z0-9_-]+$/.test(customId))
throw new Error('customId is required and may only contain letters, digits, _ and -');
const user = process.env.BROWSERSTACK_USERNAME;
const key = process.env.BROWSERSTACK_ACCESS_KEY;
if (!user || !key) throw new Error('BROWSERSTACK_USERNAME / BROWSERSTACK_ACCESS_KEY not set');

const form = new FormData();
form.append('file', new Blob([fs.readFileSync(file)]), file.split('/').pop());
form.append('custom_id', customId);

const res = await fetch('https://api-cloud.browserstack.com/app-automate/upload', {
method: 'POST',
headers: { Authorization: 'Basic ' + Buffer.from(`${user}:${key}`).toString('base64') },
body: form,
});
const text = await res.text();
if (!res.ok) throw new Error(`App Automate upload failed: ${res.status} ${text}`);
const out = JSON.parse(text);
if (!out.app_url) throw new Error(`No app_url in response: ${text}`);

console.log(`Uploaded to App Automate: ${out.app_url} (custom_id: ${out.custom_id})`);
if (out.shareable_id) console.log(`Shareable id: ${out.shareable_id}`);

if (process.env.GITHUB_OUTPUT) {
fs.appendFileSync(
process.env.GITHUB_OUTPUT,
`bs_url=${out.app_url}\ncustom_id=${out.custom_id || customId}\nshareable_id=${out.shareable_id || ''}\n`
);
}
}

main().catch((e) => {
console.error(e.message);
process.exit(1);
});
Loading