name: Capacitor Android on: workflow_call: inputs: node-version: type: string default: '24.16.0' java-version: type: string default: '21' runner: type: string default: 'ubuntu-latest' build-type: description: 'debug | release' type: string default: 'debug' output-format: description: 'apk (sideload/test) | aab (Play Store)' type: string default: 'apk' version-code: description: 'versionCode Android esplicito. Se vuoto viene derivato da package.json: major*10000 + minor*100 + patch' type: string default: '' secrets: NPM_TOKEN: required: false KEYSTORE_BASE64: required: false KEYSTORE_PASSWORD: required: false KEY_ALIAS: required: false KEY_PASSWORD: required: false GOOGLE_SERVICES_JSON: required: false jobs: android-build: runs-on: ${{ inputs.runner }} steps: - name: Checkout uses: https://github.com/actions/checkout@v6 - name: Setup Node.js uses: https://github.com/actions/setup-node@v4 with: node-version: ${{ inputs.node-version || '24.16.0' }} - name: Setup Java uses: https://github.com/actions/setup-java@v4 with: distribution: temurin java-version: ${{ inputs.java-version || '17' }} - name: Setup Android SDK uses: https://github.com/android-actions/setup-android@v4 - name: Cache npm uses: https://github.com/actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - name: Cache Gradle uses: https://github.com/actions/cache@v4 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | ${{ runner.os }}-gradle- - name: Configure npm private registry run: | echo "@pzeta:registry=https://gitea.pzetatouch.it/api/packages/PZeta_Touch/npm/" >> ~/.npmrc echo "//gitea.pzetatouch.it/api/packages/PZeta_Touch/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc - name: Read app info from package.json id: app-info run: | echo "name=$(jq -r '.name' package.json)" >> $GITHUB_OUTPUT echo "version=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT - name: Install dependencies run: npm ci - name: Build web app run: npm run build - name: Add Android platform run: npx cap add android || true - name: Sync Capacitor run: npx cap sync android # La piattaforma Android puo essere generata da 'cap add' (default Capacitor: # versionName "1.0", versionCode 1) oppure versionata nel repo consumer. # In entrambi i casi la versione pubblicata deve derivare da package.json, # unica fonte di verita, altrimenti l'APK/AAB dichiara una versione diversa # da quella del tag e Play Store rifiuta upload con versionCode non crescente. - name: Inject Android version id: android-version env: PKG_VERSION: ${{ steps.app-info.outputs.version }} VERSION_CODE_OVERRIDE: ${{ inputs.version-code }} run: | set -euo pipefail if [ -f android/app/build.gradle ]; then GRADLE_FILE="android/app/build.gradle" KTS=0 elif [ -f android/app/build.gradle.kts ]; then GRADLE_FILE="android/app/build.gradle.kts" KTS=1 else echo "::error::Nessun build.gradle in android/app/ — piattaforma Android non generata" exit 1 fi VERSION_NAME="$PKG_VERSION" if [ -n "$VERSION_CODE_OVERRIDE" ]; then VERSION_CODE="$VERSION_CODE_OVERRIDE" else CORE="${PKG_VERSION%%-*}" # scarta il pre-release: 1.2.3-beta.1 -> 1.2.3 CORE="${CORE%%+*}" # scarta il build metadata: 1.2.3+abc -> 1.2.3 MAJOR="$(echo "$CORE" | cut -d. -f1)" MINOR="$(echo "$CORE" | cut -d. -f2)" PATCH="$(echo "$CORE" | cut -d. -f3)" MINOR="${MINOR:-0}" PATCH="${PATCH:-0}" case "${MAJOR}${MINOR}${PATCH}" in ''|*[!0-9]*) echo "::error::Versione package.json non semver: '$PKG_VERSION'. Passare version-code esplicito." exit 1 ;; esac if [ "$MINOR" -gt 99 ] || [ "$PATCH" -gt 99 ]; then echo "::error::minor/patch > 99 non rappresentabili ('$PKG_VERSION'). Passare version-code esplicito." exit 1 fi VERSION_CODE=$(( MAJOR * 10000 + MINOR * 100 + PATCH )) fi if [ "$KTS" -eq 1 ]; then sed -i -E "s/versionCode[[:space:]]*=[[:space:]]*[0-9]+/versionCode = ${VERSION_CODE}/" "$GRADLE_FILE" sed -i -E "s/versionName[[:space:]]*=[[:space:]]*\"[^\"]*\"/versionName = \"${VERSION_NAME}\"/" "$GRADLE_FILE" else sed -i -E "s/versionCode[[:space:]]+[0-9]+/versionCode ${VERSION_CODE}/" "$GRADLE_FILE" sed -i -E "s/versionName[[:space:]]+\"[^\"]*\"/versionName \"${VERSION_NAME}\"/" "$GRADLE_FILE" fi # Il sed silenzioso e' peggio di un fallimento: senza questa verifica una # variazione del template Capacitor produrrebbe build con versione sbagliata. # Si confrontano i valori estratti, non si rilegge il file con una regex # costruita sulla versione: '+' e '.' di un semver sono metacaratteri ERE. # '|| true': senza, pipefail farebbe uscire lo step su un file privo di # versionCode/versionName, perdendo la diagnostica del blocco sottostante. ACTUAL_CODE="$(grep -oE 'versionCode[[:space:]]*=?[[:space:]]*[0-9]+' "$GRADLE_FILE" | head -1 | grep -oE '[0-9]+$' || true)" ACTUAL_NAME="$(grep -oE 'versionName[[:space:]]*=?[[:space:]]*"[^"]*"' "$GRADLE_FILE" | head -1 | cut -d'"' -f2 || true)" if [ "$ACTUAL_CODE" != "$VERSION_CODE" ] || [ "$ACTUAL_NAME" != "$VERSION_NAME" ]; then echo "::error::Iniezione versione fallita in $GRADLE_FILE" echo " atteso: versionName='${VERSION_NAME}' versionCode=${VERSION_CODE}" echo " trovato: versionName='${ACTUAL_NAME}' versionCode=${ACTUAL_CODE}" grep -nE 'versionCode|versionName' "$GRADLE_FILE" || true exit 1 fi echo "version-name=${VERSION_NAME}" >> $GITHUB_OUTPUT echo "version-code=${VERSION_CODE}" >> $GITHUB_OUTPUT echo "Android: versionName=${VERSION_NAME} versionCode=${VERSION_CODE} ($GRADLE_FILE)" - name: Setup google-services.json if: secrets.GOOGLE_SERVICES_JSON != '' run: echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > android/app/google-services.json - name: Setup release keystore if: inputs.build-type == 'release' run: echo '${{ secrets.KEYSTORE_BASE64 }}' | base64 -d > android/app/keystore.jks - name: Build Android (APK) if: inputs.output-format == 'apk' working-directory: android env: KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} KEY_ALIAS: ${{ secrets.KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} run: | if [ "${{ inputs.build-type }}" = "release" ]; then ./gradlew assembleRelease else ./gradlew assembleDebug fi - name: Rename APK if: inputs.output-format == 'apk' run: | APP="${{ steps.app-info.outputs.name }}-${{ steps.app-info.outputs.version }}-${{ inputs.build-type }}" APK_DIR="android/app/build/outputs/apk/${{ inputs.build-type }}" mv "$APK_DIR"/app-${{ inputs.build-type }}.apk "$APK_DIR/${APP}.apk" 2>/dev/null || \ mv "$APK_DIR"/app-${{ inputs.build-type }}-unsigned.apk "$APK_DIR/${APP}.apk" 2>/dev/null || \ find "$APK_DIR" -name "*.apk" ! -name "${APP}.apk" -exec mv {} "$APK_DIR/${APP}.apk" \; - name: Build Android (AAB) if: inputs.output-format == 'aab' working-directory: android env: KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} KEY_ALIAS: ${{ secrets.KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} run: ./gradlew bundleRelease - name: Rename AAB if: inputs.output-format == 'aab' run: | APP="${{ steps.app-info.outputs.name }}-${{ steps.app-info.outputs.version }}-release" AAB_DIR="android/app/build/outputs/bundle/release" find "$AAB_DIR" -name "*.aab" ! -name "${APP}.aab" -exec mv {} "$AAB_DIR/${APP}.aab" \; - name: Upload APK artifact if: inputs.output-format == 'apk' uses: https://github.com/actions/upload-artifact@v3 with: name: ${{ steps.app-info.outputs.name }}-${{ steps.app-info.outputs.version }}-${{ inputs.build-type }}-apk path: android/app/build/outputs/apk/${{ inputs.build-type }}/${{ steps.app-info.outputs.name }}-${{ steps.app-info.outputs.version }}-${{ inputs.build-type }}.apk retention-days: 14 - name: Upload AAB artifact if: inputs.output-format == 'aab' uses: https://github.com/actions/upload-artifact@v3 with: name: ${{ steps.app-info.outputs.name }}-${{ steps.app-info.outputs.version }}-release-aab path: android/app/build/outputs/bundle/release/${{ steps.app-info.outputs.name }}-${{ steps.app-info.outputs.version }}-release.aab retention-days: 14 - name: Upload Gradle build reports if: always() uses: https://github.com/actions/upload-artifact@v3 with: name: gradle-build-reports path: android/build/reports/ retention-days: 7 if-no-files-found: ignore