Settings: per-permission "why" popup (with Open settings shortcut) #360
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |