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
91 changes: 91 additions & 0 deletions .github/workflows/build-apk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Build release APK

on:
pull_request:
types: [closed]
branches: [main]

concurrency:
group: build-apk
cancel-in-progress: false

jobs:
build:
# 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: 20
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"

- 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 }}"

- uses: actions/upload-artifact@v4
with:
name: ${{ steps.apk.outputs.name }}
path: ${{ steps.apk.outputs.name }}
retention-days: 7
Comment on lines +15 to +91
2 changes: 1 addition & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"expo": {
"name": "BStack Bank",
"slug": "yash",
"version": "2.0.0",
"version": "2.0.1",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "demobankingapp",
Expand Down
66 changes: 66 additions & 0 deletions scripts/make-pr.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Usage: ./scripts/make-pr.sh "your commit message"
# Commits your current changes to branch pr/workflow as "feat(dev): <message>" and opens a PR to main.
# Safe to run repeatedly: pr/workflow is always rebuilt from the latest origin/main,
# so leftovers from previously merged PRs never conflict.
set -euo pipefail

BRANCH="pr/workflow"
BASE="main"
PREFIX="feat(dev): "

MSG="${*:-}"
if [ -z "$MSG" ]; then
read -r -p "Commit message: " MSG
fi
[ -z "$MSG" ] && { echo "Commit message required"; exit 1; }
MSG="${MSG#"$PREFIX"}" # avoid double prefix
TITLE="${PREFIX}${MSG}"

command -v gh >/dev/null || { echo "gh CLI required"; exit 1; }

git fetch origin --prune

# If an OPEN PR already exists for this branch, just add the commit to it.
OPEN_PR=""
if gh pr list --head "$BRANCH" --state open --json url -q '.[0].url' 2>/dev/null | grep -q http; then
OPEN_PR=$(gh pr list --head "$BRANCH" --state open --json url -q '.[0].url')
fi

if [ -n "$OPEN_PR" ]; then
# Continue the existing open PR
CUR=$(git rev-parse --abbrev-ref HEAD)
[ "$CUR" != "$BRANCH" ] && git checkout "$BRANCH"
git add -A
git diff --cached --quiet || git commit -m "$TITLE"
git push origin "$BRANCH"
echo "Open PR updated: $OPEN_PR"
echo "Note: the PR title must still start with '$PREFIX' to trigger the build."
exit 0
fi

# No open PR: carry current changes onto a fresh branch cut from the latest origin/main.
git add -A
STASHED=0
if ! git diff --cached --quiet; then
git stash push -q -m "make-pr-wip"
STASHED=1
fi

git checkout -B "$BRANCH" "origin/$BASE"

if [ "$STASHED" = 1 ]; then
git stash pop -q || { echo "Conflict applying your changes onto latest $BASE. Resolve, then re-run."; exit 1; }
fi

git add -A
if git diff --cached --quiet; then
echo "No changes to commit (nothing differs from origin/$BASE)."
exit 1
fi
git commit -m "$TITLE"

# Branch is rebuilt from main each time, so a force push is intended (only this branch).
git push -u --force-with-lease origin "$BRANCH"

gh pr create --base "$BASE" --head "$BRANCH" --title "$TITLE" --body "$TITLE"
80 changes: 80 additions & 0 deletions scripts/upload-to-drive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env node
// Uploads a file to a Google Drive folder using a service account.
// Usage: node scripts/upload-to-drive.js <file> [remoteName]
// Env: GDRIVE_SERVICE_ACCOUNT_JSON (raw JSON), GDRIVE_FOLDER_ID
// No dependencies: uses Node 18+ fetch and crypto.
const fs = require('fs');
const crypto = require('crypto');

const b64url = (b) =>
Buffer.from(b).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');

async function getToken(sa) {
const now = Math.floor(Date.now() / 1000);
const header = b64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
const claim = b64url(
JSON.stringify({
iss: sa.client_email,
scope: 'https://www.googleapis.com/auth/drive',
aud: 'https://oauth2.googleapis.com/token',
iat: now,
exp: now + 3600,
})
);
const sig = crypto.createSign('RSA-SHA256').update(`${header}.${claim}`).sign(sa.private_key);
const res = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: `${header}.${claim}.${b64url(sig)}`,
}),
});
if (!res.ok) throw new Error(`Token request failed: ${res.status} ${await res.text()}`);
return (await res.json()).access_token;
}

async function main() {
const [file, remoteName] = process.argv.slice(2);
if (!file || !fs.existsSync(file)) throw new Error(`File not found: ${file}`);
const folderId = (process.env.GDRIVE_FOLDER_ID || '').trim();
if (!folderId) throw new Error('GDRIVE_FOLDER_ID is not set');
let raw = (process.env.GDRIVE_SERVICE_ACCOUNT_JSON || '').trim();
if (!raw) throw new Error('GDRIVE_SERVICE_ACCOUNT_JSON is not set');
if (!raw.startsWith('{')) raw = Buffer.from(raw, 'base64').toString('utf8'); // allow base64 too
const sa = JSON.parse(raw);

const token = await getToken(sa);
const name = remoteName || file.split('/').pop();
const boundary = 'bndry' + crypto.randomBytes(8).toString('hex');
const meta = JSON.stringify({ name, parents: [folderId] });
const body = Buffer.concat([
Buffer.from(
`--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n${meta}\r\n` +
`--${boundary}\r\nContent-Type: application/vnd.android.package-archive\r\n\r\n`
),
fs.readFileSync(file),
Buffer.from(`\r\n--${boundary}--`),
]);

const res = await fetch(
'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&supportsAllDrives=true&fields=id,name,webViewLink',
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': `multipart/related; boundary=${boundary}`,
},
body,
}
);
if (!res.ok) throw new Error(`Upload failed: ${res.status} ${await res.text()}`);
const out = await res.json();
console.log(`Uploaded "${out.name}" (id: ${out.id})`);
if (out.webViewLink) console.log(out.webViewLink);
}

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