Skip to content

Settings: per-permission "why" popup (with Open settings shortcut) #360

Settings: per-permission "why" popup (with Open settings shortcut)

Settings: per-permission "why" popup (with Open settings shortcut) #360

name: Merged Build & Release
on:
push:
branches: [ "**" ]
pull_request:
branches: [ "main" ]
workflow_dispatch:
# ⚡ Concurrency: Cancels the entire job if a new push comes in.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-release:
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
checks: read
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Inject Google Services
env:
GOOGLE_SERVICES_API_KEY: ${{ secrets.GOOGLE_SERVICES_API_KEY }}
PROJECT_ID: ${{ secrets.PROJECT_ID }}
CLIENT_ID: ${{ secrets.CLIENT_ID }}
run: |
if [ -f "app/google-services.json.template" ]; then
echo "Template found. Injecting secrets..."
sed -e 's/{{\([^}]*\)}}/${\1}/g' app/google-services.json.template | envsubst > app/google-services.json
else
echo "Warning: google-services.json.template not found. Skipping injection."
fi
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: false
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Prepare Keystore from Secrets
env:
KEYSTORE_PRIVATE: ${{ secrets.KEYSTORE_PRIVATE }}
KEYSTORE_RSA: ${{ secrets.KEYSTORE_RSA }}
KEYSTORE_CHAIN: ${{ secrets.KEYSTORE_CHAIN }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEYSTORE_OWNER: ${{ secrets.KEYSTORE_OWNER }}
KEYSTORE_SHA1: ${{ secrets.KEYSTORE_SHA1 }}
KEYSTORE_SHA256: ${{ secrets.KEYSTORE_SHA256 }}
KEYSTORE_PUBLIC: ${{ secrets.KEYSTORE_PUBLIC }}
run: |
set -e
PRIVATE_KEY="${KEYSTORE_PRIVATE:-$KEYSTORE_RSA}"
if [ -n "$PRIVATE_KEY" ] && [ -n "$KEYSTORE_CHAIN" ]; then
echo "Reconstructing Keystore from Secrets..."
echo "$PRIVATE_KEY" > private.key
echo "$KEYSTORE_CHAIN" > chain.crt
# Create PKCS12 Keystore
# Attempt to handle encrypted private key if KEY_PASSWORD is set
PASSIN_ARG=""
if [ -n "$KEY_PASSWORD" ]; then
PASSIN_ARG="-passin env:KEY_PASSWORD"
fi
# Use legacy algorithms for Java compatibility if using OpenSSL 3
# Also explicit pass: scheme to avoid env reading issues on some runners
openssl pkcs12 -export -in chain.crt -inkey private.key -out release.keystore \
-name "$KEY_ALIAS" -passout pass:"$KEYSTORE_PASSWORD" $PASSIN_ARG -legacy
# Cleanup raw keys immediately
rm -f private.key chain.crt
# Verify the keystore immediately to catch password/format issues
echo "Verifying generated keystore..."
keytool -list -keystore release.keystore -storepass "$KEYSTORE_PASSWORD" -v
echo "KEYSTORE_FILE=$(pwd)/release.keystore" >> $GITHUB_ENV
# Ensure Gradle uses the same password for the key as the store (OpenSSL default behavior)
echo "KEY_PASSWORD<<EOF" >> $GITHUB_ENV
echo "$KEYSTORE_PASSWORD" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
else
echo "Warning: KEYSTORE_PRIVATE/KEYSTORE_RSA or KEYSTORE_CHAIN not set. Skipping release signing setup."
fi
- name: Build with Gradle
id: gradle-build
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
# KEYSTORE_FILE is set in previous step if successful
run: |
set +e
export BUILD_NUMBER=$(git rev-list --count HEAD)
# Build the APK (Release)
if [ -n "$KEYSTORE_FILE" ]; then
echo "Building Release APK with Signing..."
./gradlew assembleRelease -PversionBuild=$BUILD_NUMBER --build-cache > build.log 2>&1
else
echo "Building Debug APK (Fallback)..."
./gradlew assembleDebug -PversionBuild=$BUILD_NUMBER --build-cache > build.log 2>&1
fi
EXIT_CODE=$?
echo "exit_code=$EXIT_CODE" >> $GITHUB_OUTPUT
cat build.log
exit $EXIT_CODE
- name: Destroy Keystore
if: always()
run: |
rm -f release.keystore
# ------------------------------------------------------------------
# FAILURE HANDLING
# ------------------------------------------------------------------
- name: Report Failure to Jules
if: failure() && steps.gradle-build.outcome == 'failure'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
let logTail = 'Log file not found.';
try { logTail = fs.readFileSync('build.log', 'utf8').slice(-4000); } catch (e) {}
const assignee = 'gemini-code-assist';
try {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Build Failure on ${context.eventName === 'pull_request' ? 'PR #' + context.issue.number : context.ref}`,
body: `@${assignee} /jules debug this build error\n\n**Log:**\n\`\`\`\n${logTail}\n\`\`\``,
labels: ['jules', 'bug'],
assignees: [assignee]
});
} catch (error) {
console.log("Failed to create issue with assignee. Retrying without assignee.");
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Build Failure on ${context.eventName === 'pull_request' ? 'PR #' + context.issue.number : context.ref}`,
body: `@${assignee} /jules debug this build error\n\n**Log:**\n\`\`\`\n${logTail}\n\`\`\``,
labels: ['jules', 'bug']
});
}
# ------------------------------------------------------------------
# RELEASE STEPS (Only run on PUSH, not Pull Requests)
# ------------------------------------------------------------------
- name: Prepare Release Variables
if: success() && github.event_name == 'push'
id: release_vars
run: |
export BUILD_NUMBER=$(git rev-list --count HEAD)
# Extract version info from properties file
MAJOR=$(grep 'major=' version.properties | cut -d'=' -f2)
MINOR=$(grep 'minor=' version.properties | cut -d'=' -f2)
# Calculate Patch version programmatically based on commits since Minor update
MINOR_COMMIT=$(git blame -L '/minor=/',+1 version.properties | awk '{print $1}' | tr -d '^')
if [ -n "$MINOR_COMMIT" ]; then
PATCH=$(git rev-list --count $MINOR_COMMIT..HEAD)
else
PATCH=$(grep 'patch=' version.properties | cut -d'=' -f2)
fi
VERSION_NAME="$MAJOR.$MINOR.$PATCH.$BUILD_NUMBER"
TAG_NAME="latest-release-v${MAJOR}.${MINOR}"
# Dynamic App Name from settings.gradle
if [ -f "settings.gradle.kts" ]; then
APP_NAME=$(grep 'rootProject.name' settings.gradle.kts | sed -E "s/.*=.*[\"'](.*)[\"']/\1/")
elif [ -f "settings.gradle" ]; then
APP_NAME=$(grep 'rootProject.name' settings.gradle | sed -E "s/.*=.*[\"'](.*)[\"']/\1/")
else
APP_NAME="App"
fi
# Locate built APK (Prefer Release, fallback to Debug)
APK_ORIGINAL=$(find app/build/outputs/apk/release -name "*.apk" | head -n 1)
if [ -z "$APK_ORIGINAL" ]; then
APK_ORIGINAL=$(find app/build/outputs/apk/debug -name "*.apk" | head -n 1)
SUFFIX="-debug"
else
SUFFIX="-release"
fi
if [ -z "$APK_ORIGINAL" ]; then
echo "Error: No APK found to release!"
exit 1
fi
TARGET_NAME="${APP_NAME}-${VERSION_NAME}${SUFFIX}.apk"
# Rename for release
mv "$APK_ORIGINAL" "$TARGET_NAME"
echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV
echo "APK_FILE=$TARGET_NAME" >> $GITHUB_ENV
- name: Publish Release
if: success() && github.event_name == 'push'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Force update tag to point to current commit
git tag -fa $TAG_NAME -m "Latest Debug Build"
git push origin $TAG_NAME --force
# Check if release exists
if gh release view $TAG_NAME > /dev/null 2>&1; then
echo "Updating existing release..."
gh release edit $TAG_NAME --prerelease \
--title "Latest Release ($TAG_NAME)" \
--notes "Auto-build from commit $GITHUB_SHA" \
--target $GITHUB_SHA
gh release upload $TAG_NAME "$APK_FILE" --clobber
else
echo "Creating new release..."
gh release create $TAG_NAME "$APK_FILE" --prerelease \
--title "Latest Release ($TAG_NAME)" \
--notes "Auto-build from commit $GITHUB_SHA" \
--target $GITHUB_SHA
fi