8 Commits

Author SHA1 Message Date
billisdead d3d73c255e feat: add CHANGELOG.md and auto-create Gitea release on tag push
Release APK / build (push) Has been cancelled
Release body now includes conventional-commit bullet points, a compare
link (prev_tag...current_tag) and a link to CHANGELOG.md for both the
GitHub release and a new Gitea release created via API.
GITEA_TOKEN secret must be added to GitHub repo secrets to enable
the Gitea release step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 11:27:28 +02:00
billisdead 72cfbf41f3 fix: revert splash screen overrides, remove redundant SafeAreaProvider
expo-router 56 already calls _internal_preventAutoHideAsync() internally.
Calling preventAutoHideAsync() at module level created a second user lock
that blocked the native splash indefinitely on Android, causing a crash.
SafeAreaProvider removed as expo-router's ExpoRoot already wraps with it.
Keep SplashScreen.hideAsync() to release the splash after SecureStore reads.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 11:26:24 +02:00
billisdead 86303e5c84 fix: prevent black screen on launch by holding splash until settings ready
Release APK / build (push) Has been cancelled
Add SplashScreen.preventAutoHideAsync() at module level and hideAsync()
once SecureStore reads complete. Add splash backgroundColor to app.json
to match app theme. Wrap layout in SafeAreaProvider.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 09:00:39 +02:00
billisdead c414ddc4fa fix: use printf %q to escape special chars in signing.env values
Release APK / build (push) Has been cancelled
2026-06-19 18:26:13 +02:00
billisdead f0fda1947d ci: add signing.env verification step to debug secret injection
Release APK / build (push) Has been cancelled
2026-06-19 18:24:59 +02:00
billisdead 8a75cffbd1 fix: inject signing secrets via env vars, write signing.env with printf
Release APK / build (push) Has been cancelled
2026-06-19 18:22:08 +02:00
billisdead 0b0226368b fix: quote heredoc delimiter to prevent shell expansion of secret values
Release APK / build (push) Has been cancelled
2026-06-19 18:18:13 +02:00
billisdead 8f42971187 ci: switch to signed release APK via build-apk.sh
Release APK / build (push) Has been cancelled
Debug APK required Metro bundler at runtime. Switch to a signed release
build using a local keystore (same pattern as Postiz-Android): keystore
decoded from KEYSTORE_B64 secret, build-apk.sh patches build.gradle and
assembles a standalone APK.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 18:07:22 +02:00
5 changed files with 343 additions and 29 deletions
+125 -28
View File
@@ -1,9 +1,9 @@
name: Build Android APK
name: Release APK
on:
push:
tags:
- 'v*'
- 'v*.*.*'
workflow_dispatch:
concurrency:
@@ -12,7 +12,9 @@ concurrency:
jobs:
build:
if: github.server_url == 'https://github.com'
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: write
@@ -24,10 +26,14 @@ jobs:
sudo rm -rf /usr/share/swift /usr/local/julia*
sudo apt-get autoremove -y > /dev/null 2>&1
sudo apt-get clean
df -h
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- uses: actions/setup-node@v4
with:
node-version: '20'
@@ -36,10 +42,15 @@ jobs:
- name: Install dependencies
run: npm ci
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Set up Android SDK
uses: android-actions/setup-android@v3
- name: Accept Android SDK licenses
run: yes | sdkmanager --licenses || true
- name: Install SDK components
run: |
sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0"
- name: Cache Gradle
uses: actions/cache@v4
@@ -47,31 +58,117 @@ jobs:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: gradle-
key: ${{ runner.os }}-gradle-${{ hashFiles('package.json') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Generate Android native project
run: npx expo prebuild --platform android --clean
- name: Build debug APK
working-directory: android
- name: Decode keystore
env:
KEYSTORE_B64: ${{ secrets.KEYSTORE_B64 }}
KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }}
KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }}
KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }}
run: |
chmod +x gradlew
./gradlew assembleDebug --no-daemon
mkdir -p ~/.config/sheethappens
echo "$KEYSTORE_B64" | base64 -d > ~/.config/sheethappens/sheethappens.jks
{
printf 'KEYSTORE_PATH=%s\n' "$HOME/.config/sheethappens/sheethappens.jks"
printf 'KEYSTORE_ALIAS=%q\n' "$KEYSTORE_ALIAS"
printf 'KEYSTORE_STORE_PASSWORD=%q\n' "$KEYSTORE_STORE_PASSWORD"
printf 'KEYSTORE_KEY_PASSWORD=%q\n' "$KEYSTORE_KEY_PASSWORD"
} > ~/.config/sheethappens/signing.env
chmod 600 ~/.config/sheethappens/signing.env ~/.config/sheethappens/sheethappens.jks
- name: Upload APK artifact
uses: actions/upload-artifact@v4
with:
name: SheetHappens-debug-${{ github.ref_name }}
path: android/app/build/outputs/apk/debug/app-debug.apk
retention-days: 30
- name: Verify signing.env
run: |
echo "Keys present in signing.env:"
grep -o '^[^=]*' ~/.config/sheethappens/signing.env
source ~/.config/sheethappens/signing.env
[ -n "$KEYSTORE_PATH" ] && echo "KEYSTORE_PATH: set" || echo "KEYSTORE_PATH: EMPTY"
[ -n "$KEYSTORE_ALIAS" ] && echo "KEYSTORE_ALIAS: set" || echo "KEYSTORE_ALIAS: EMPTY"
[ -n "$KEYSTORE_STORE_PASSWORD" ] && echo "KEYSTORE_STORE_PASSWORD: set" || echo "KEYSTORE_STORE_PASSWORD: EMPTY"
[ -n "$KEYSTORE_KEY_PASSWORD" ] && echo "KEYSTORE_KEY_PASSWORD: set" || echo "KEYSTORE_KEY_PASSWORD: EMPTY"
- name: Build signed APK
run: ./build-apk.sh
- name: Find built APK
id: apk
run: |
APK=$(ls dist/*.apk | sort | tail -1)
echo "path=$APK" >> "$GITHUB_OUTPUT"
- name: Generate changelog
id: changelog
run: |
PREV_TAG=$(git tag --sort=-version:refname | grep -v "^${{ github.ref_name }}$" | head -1)
echo "Previous tag: $PREV_TAG"
echo "prev_tag=$PREV_TAG" >> "$GITHUB_OUTPUT"
FEATS=$(git log "${PREV_TAG}..HEAD" --pretty=format:"%s" --no-merges \
| grep -E "^feat(\([^)]+\))?: " \
| sed -E 's/^feat(\([^)]+\))?: //' \
| sed 's/^/- /')
FIXES=$(git log "${PREV_TAG}..HEAD" --pretty=format:"%s" --no-merges \
| grep -E "^fix(\([^)]+\))?: " \
| sed -E 's/^fix(\([^)]+\))?: //' \
| sed 's/^/- /')
{
echo "changelog<<CEOF"
[ -n "$FEATS" ] && printf "### What's New\n%s\n\n" "$FEATS"
[ -n "$FIXES" ] && printf "### Bug Fixes\n%s\n\n" "$FIXES"
echo "CEOF"
} >> "$GITHUB_OUTPUT"
- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
name: "SheetHappens ${{ github.ref_name }}"
body: |
## SheetHappens ${{ github.ref_name }}
${{ steps.changelog.outputs.changelog }}
[Full changelog](https://github.com/${{ github.repository }}/compare/${{ steps.changelog.outputs.prev_tag }}...${{ github.ref_name }}) · [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md)
### Installation
1. Enable "Unknown sources" on the device
2. Transfer the APK to the device and open it to install
files: ${{ steps.apk.outputs.path }}
draft: false
prerelease: ${{ contains(github.ref_name, '-') }}
- name: Create Gitea Release
env:
GH_TOKEN: ${{ github.token }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
TAG: ${{ github.ref_name }}
PREV_TAG: ${{ steps.changelog.outputs.prev_tag }}
CHANGELOG: ${{ steps.changelog.outputs.changelog }}
GH_REPO: ${{ github.repository }}
run: |
gh release create ${{ github.ref_name }} \
android/app/build/outputs/apk/debug/app-debug.apk#SheetHappens-${{ github.ref_name }}.apk \
--title "SheetHappens ${{ github.ref_name }}" \
--generate-notes
if [ -z "$GITEA_TOKEN" ]; then
echo "GITEA_TOKEN not set — skipping Gitea release"
exit 0
fi
BODY="## SheetHappens ${TAG}
${CHANGELOG}
[Full changelog](https://homegit.gyozamancave.fr/billisdead/SheetHappens/compare/${PREV_TAG}...${TAG}) · [CHANGELOG.md](https://homegit.gyozamancave.fr/billisdead/SheetHappens/raw/branch/main/CHANGELOG.md)
### Installation
Download the APK from the [GitHub release](https://github.com/${GH_REPO}/releases/tag/${TAG}), enable \"Unknown sources\" on the device and install."
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"https://homegit.gyozamancave.fr/api/v1/repos/billisdead/SheetHappens/releases" \
-d "{
\"tag_name\": \"${TAG}\",
\"name\": \"SheetHappens ${TAG}\",
\"body\": $(printf '%s' "$BODY" | jq -Rs .),
\"draft\": false,
\"prerelease\": $([ "${TAG#*-}" != "${TAG}" ] && echo true || echo false)
}"
+46
View File
@@ -0,0 +1,46 @@
# Changelog
All notable changes to SheetHappens are documented here.
Each release links to the full diff for detailed context.
---
## [v0.1.7] — 2026-06-22
### Bug Fixes
- Revert splash screen overrides that caused Android crash-to-desktop — expo-router already manages the splash lifecycle internally
- Remove redundant `SafeAreaProvider` wrapper (expo-router's `ExpoRoot` already injects it)
[View changes](https://github.com/pirona/SheetHappens/compare/v0.1.6...v0.1.7)
---
## [v0.1.6] — 2026-06-22
### Bug Fixes
- Prevent black screen on launch by holding splash until SecureStore settings load
- Add `splash.backgroundColor` to `app.json` to match app theme on Android
[View changes](https://github.com/pirona/SheetHappens/compare/v0.1.5...v0.1.6)
---
## [v0.1.5] — 2026-06-22
### Bug Fixes
- Fix signed APK release: inject signing secrets via env vars, write `signing.env` with `printf %q` to handle special characters
- Add `signing.env` verification step in CI
[View changes](https://github.com/pirona/SheetHappens/compare/v0.1.0...v0.1.5)
---
## [v0.1.0] — Initial release
### Features
- Incident log with SQLite persistence
- Voice input via `expo-speech-recognition`
- Edit mode, service autocomplete
- Dark theme (NativeWind + expo-router)
- Onboarding flow
- Signed APK via GitHub Actions CI
+3
View File
@@ -6,6 +6,9 @@
"orientation": "portrait",
"userInterfaceStyle": "dark",
"backgroundColor": "#0f1117",
"splash": {
"backgroundColor": "#0f1117"
},
"scheme": "sheethappens",
"android": {
"adaptiveIcon": {
+6 -1
View File
@@ -1,5 +1,5 @@
import { useEffect } from "react";
import { Stack, router } from "expo-router";
import { Stack, router, SplashScreen } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import "../global.css";
@@ -9,6 +9,11 @@ import { useSettings } from "@/hooks/useSettings";
export default function RootLayout() {
const { settings, ready } = useSettings();
useEffect(() => {
if (!ready) return;
SplashScreen.hideAsync();
}, [ready]);
useEffect(() => {
if (!ready) return;
if (!settings.onboardingDone) {
Executable
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env bash
# build-apk.sh — Build a signed release APK locally without expo.dev / EAS.
#
# Prerequisites (first time only):
# 1. Generate a keystore:
# keytool -genkey -v -keystore ~/.config/sheethappens/sheethappens.jks \
# -alias sheethappens -keyalg RSA -keysize 2048 -validity 10000
# 2. Fill in signing credentials:
# mkdir -p ~/.config/sheethappens
# cat > ~/.config/sheethappens/signing.env <<EOF
# KEYSTORE_PATH=~/.config/sheethappens/sheethappens.jks
# KEYSTORE_ALIAS=sheethappens
# KEYSTORE_STORE_PASSWORD=your_store_password
# KEYSTORE_KEY_PASSWORD=your_key_password
# EOF
# chmod 600 ~/.config/sheethappens/signing.env
#
# Usage:
# ./build-apk.sh # → dist/sheethappens-YYYYMMDD-HHMM.apk
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SIGNING_ENV="$HOME/.config/sheethappens/signing.env"
DIST_DIR="$SCRIPT_DIR/dist"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
info() { echo -e "${GREEN}[build]${NC} $*"; }
warn() { echo -e "${YELLOW}[warn]${NC} $*"; }
abort() { echo -e "${RED}[error]${NC} $*" >&2; exit 1; }
# ─── 1. Signing credentials ────────────────────────────────────────────────
if [ ! -f "$SIGNING_ENV" ]; then
abort "Missing signing.env — see comments at top of this script."
fi
# shellcheck source=/dev/null
source "$SIGNING_ENV"
[ -z "${KEYSTORE_PATH:-}" ] && abort "KEYSTORE_PATH not set in signing.env"
[ -z "${KEYSTORE_ALIAS:-}" ] && abort "KEYSTORE_ALIAS not set in signing.env"
[ -z "${KEYSTORE_STORE_PASSWORD:-}" ] && abort "KEYSTORE_STORE_PASSWORD not set in signing.env"
[ -z "${KEYSTORE_KEY_PASSWORD:-}" ] && abort "KEYSTORE_KEY_PASSWORD not set in signing.env"
KEYSTORE_PATH_EXPANDED="${KEYSTORE_PATH/#\$HOME/$HOME}"
KEYSTORE_PATH_EXPANDED="${KEYSTORE_PATH_EXPANDED/#~/$HOME}"
[ ! -f "$KEYSTORE_PATH_EXPANDED" ] && \
abort "Keystore not found: $KEYSTORE_PATH_EXPANDED"
# ─── 2. Java check ─────────────────────────────────────────────────────────
if [ -z "${JAVA_HOME:-}" ] || "$JAVA_HOME/bin/java" -version 2>&1 | grep -qE '"(2[5-9]|[3-9][0-9])\.'; then
if [ -d "$HOME/jdk21" ]; then
export JAVA_HOME="$HOME/jdk21"
export PATH="$JAVA_HOME/bin:$PATH"
info "Using local JDK 21: $JAVA_HOME"
else
abort "Java 25+ detected but Gradle only supports ≤ Java 24.\nInstall JDK 21 and set JAVA_HOME."
fi
fi
info "Java: $("$JAVA_HOME/bin/java" -version 2>&1 | head -1)"
# ─── 3. Android SDK ────────────────────────────────────────────────────────
if [ -z "${ANDROID_HOME:-}" ]; then
for candidate in "$HOME/android-sdk" "$HOME/Android/Sdk" "/opt/android-sdk"; do
if [ -d "$candidate/platform-tools" ]; then
export ANDROID_HOME="$candidate"
break
fi
done
fi
[ -z "${ANDROID_HOME:-}" ] && abort "Android SDK not found. Set ANDROID_HOME."
export PATH="$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin"
info "Android SDK: $ANDROID_HOME"
# ─── 4. expo prebuild ──────────────────────────────────────────────────────
info "Running expo prebuild (Android)…"
cd "$SCRIPT_DIR"
npx expo prebuild --platform android --clean --no-install
# ─── 5. Patch build.gradle — inject release signingConfig ──────────────────
info "Patching android/app/build.gradle for release signing…"
python3 - "$SCRIPT_DIR/android/app/build.gradle" << 'PYEOF'
import sys, re
path = sys.argv[1]
with open(path, 'r') as f:
content = f.read()
if 'MYAPP_UPLOAD_STORE_FILE' in content:
print('[patch] build.gradle already patched, skipping.')
sys.exit(0)
release_signing = """\n release {
if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) {
storeFile file(MYAPP_UPLOAD_STORE_FILE)
storePassword MYAPP_UPLOAD_STORE_PASSWORD
keyAlias MYAPP_UPLOAD_KEY_ALIAS
keyPassword MYAPP_UPLOAD_KEY_PASSWORD
}
}"""
content = re.sub(
r'(signingConfigs\s*\{[\s\S]*?debug\s*\{[\s\S]*?\})',
r'\1' + release_signing,
content,
count=1
)
def replace_release_signing(m):
return m.group(0).replace('signingConfig signingConfigs.debug',
'signingConfig signingConfigs.release', 1)
content = re.sub(
r'release\s*\{[^}]*signingConfig\s+signingConfigs\.debug',
replace_release_signing,
content,
count=1,
flags=re.DOTALL
)
with open(path, 'w') as f:
f.write(content)
print('[patch] build.gradle patched for release signing.')
PYEOF
# ─── 6. Inject signing props into gradle.properties ────────────────────────
info "Injecting signing credentials into gradle.properties…"
GRADLE_PROPS="$SCRIPT_DIR/android/gradle.properties"
sed -i '/^# --- sheethappens release signing/,/^# --- end signing/d' "$GRADLE_PROPS" 2>/dev/null || true
cat >> "$GRADLE_PROPS" << EOF
# --- sheethappens release signing (injected by build-apk.sh — wiped after build)
MYAPP_UPLOAD_STORE_FILE=$KEYSTORE_PATH_EXPANDED
MYAPP_UPLOAD_STORE_PASSWORD=$KEYSTORE_STORE_PASSWORD
MYAPP_UPLOAD_KEY_ALIAS=$KEYSTORE_ALIAS
MYAPP_UPLOAD_KEY_PASSWORD=$KEYSTORE_KEY_PASSWORD
# --- end signing
EOF
# ─── 7. Gradle build ───────────────────────────────────────────────────────
cd "$SCRIPT_DIR/android"
chmod +x gradlew
info "Building APK (release)…"
./gradlew assembleRelease --no-daemon
# ─── 8. Wipe credentials from gradle.properties ───────────────────────────
sed -i '/^# --- sheethappens release signing/,/^# --- end signing/d' "$GRADLE_PROPS"
info "Signing credentials wiped from gradle.properties."
# ─── 9. Copy to dist/ ─────────────────────────────────────────────────────
mkdir -p "$DIST_DIR"
TIMESTAMP="$(date +%Y%m%d-%H%M)"
OUTPUT="$DIST_DIR/sheethappens-$TIMESTAMP.apk"
cp "app/build/outputs/apk/release/app-release.apk" "$OUTPUT"
echo ""
info "Build complete!"
echo -e " ${GREEN}$OUTPUT${NC}"
echo ""