Release #5
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: Release | |
| on: | |
| workflow_dispatch: | |
| permissions: | |
| contents: read # jobs that need more declare it at job level | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| env: | |
| ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} | |
| ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} | |
| PLAY_STORE_CONFIG_JSON: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }} | |
| outputs: | |
| version_name: ${{ steps.meta.outputs.version_name }} | |
| hashes: ${{ steps.hashes.outputs.hashes }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set version name | |
| id: meta | |
| run: | | |
| VER=$(grep versionName ./app/build.gradle.kts | awk -F '"' '{print $2}') | |
| echo "version_name=$VER" >> $GITHUB_OUTPUT | |
| - uses: actions/setup-java@v4 | |
| with: | |
| distribution: 'zulu' | |
| java-version: '17' | |
| cache: 'gradle' | |
| - run: chmod +x ./gradlew | |
| - name: Restore keystore | |
| run: echo "${{ secrets.KEYSTORE_B64 }}" | base64 --decode > ./app/release.jks | |
| # APK build must run first: build-aab.sh strips Play Store restricted | |
| # permissions from AndroidManifest.xml, which must not affect APK output. | |
| - name: Build APKs (arm64-v8a + armeabi-v7a) | |
| run: bash build-apk.sh | |
| # build-aab.sh patches the manifest, builds the AAB, and writes play-config.json. | |
| # publishGoogleReleaseBundle uploads the AAB to Play Store internal track. | |
| - name: Build AAB and publish to Play Store | |
| run: bash build-aab.sh && ./gradlew publishGoogleReleaseBundle | |
| - name: Compute SHA-256 hashes | |
| id: hashes | |
| run: | | |
| VERSION="${{ steps.meta.outputs.version_name }}" | |
| echo "hashes=$(sha256sum \ | |
| "PlainApp-${VERSION}-default.apk" \ | |
| "PlainApp-${VERSION}-armeabi-v7a.apk" \ | |
| "PlainApp-${VERSION}.aab" \ | |
| | base64 -w0)" >> $GITHUB_OUTPUT | |
| - name: Upload release artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: release-artifacts | |
| path: | | |
| PlainApp-${{ steps.meta.outputs.version_name }}-default.apk | |
| PlainApp-${{ steps.meta.outputs.version_name }}-armeabi-v7a.apk | |
| PlainApp-${{ steps.meta.outputs.version_name }}.aab | |
| if-no-files-found: error | |
| retention-days: 1 | |
| # SLSA Level 3 provenance covers all three release artifacts. | |
| provenance: | |
| needs: build | |
| permissions: | |
| actions: read | |
| id-token: write | |
| contents: write | |
| uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 | |
| with: | |
| base64-subjects: ${{ needs.build.outputs.hashes }} | |
| upload-assets: false | |
| # Scans all three artifacts concurrently to minimise wall-clock time. | |
| virustotal: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| outputs: | |
| default_report: ${{ steps.scan.outputs.default_report }} | |
| default_status: ${{ steps.scan.outputs.default_status }} | |
| armv7_report: ${{ steps.scan.outputs.armv7_report }} | |
| armv7_status: ${{ steps.scan.outputs.armv7_status }} | |
| aab_report: ${{ steps.scan.outputs.aab_report }} | |
| aab_status: ${{ steps.scan.outputs.aab_status }} | |
| steps: | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| name: release-artifacts | |
| - name: Scan with VirusTotal | |
| id: scan | |
| env: | |
| VT_API_KEY: ${{ secrets.VIRUSTOTAL_API_KEY }} | |
| VERSION: ${{ needs.build.outputs.version_name }} | |
| run: | | |
| upload_and_poll() { | |
| local file="$1" outprefix="$2" | |
| local sha256 size_bytes upload_url analysis_id last_response poll_status | |
| local malicious suspicious undetected harmless total detected result_status | |
| sha256=$(sha256sum "$file" | awk '{print $1}') | |
| size_bytes=$(stat -c%s "$file") | |
| upload_url="https://www.virustotal.com/api/v3/files" | |
| if [ "$size_bytes" -gt 33554432 ]; then | |
| upload_url=$(curl -sS --request GET \ | |
| --url https://www.virustotal.com/api/v3/files/upload_url \ | |
| --header "x-apikey: $VT_API_KEY" | jq -r '.data') | |
| if [ -z "$upload_url" ] || [ "$upload_url" = "null" ]; then | |
| printf '%s' "https://www.virustotal.com/gui/file/$sha256/detection" > "${outprefix}.url" | |
| printf '%s' "⬜ N/A" > "${outprefix}.status" | |
| return | |
| fi | |
| fi | |
| analysis_id=$(curl -sS --request POST \ | |
| --url "$upload_url" \ | |
| --header "x-apikey: $VT_API_KEY" \ | |
| --form "file=@$file" | jq -r '.data.id') | |
| if [ -z "$analysis_id" ] || [ "$analysis_id" = "null" ]; then | |
| printf '%s' "https://www.virustotal.com/gui/file/$sha256/detection" > "${outprefix}.url" | |
| printf '%s' "⬜ N/A" > "${outprefix}.status" | |
| return | |
| fi | |
| last_response="" | |
| for i in $(seq 1 30); do | |
| sleep 20 | |
| last_response=$(curl -sS \ | |
| --url "https://www.virustotal.com/api/v3/analyses/$analysis_id" \ | |
| --header "x-apikey: $VT_API_KEY") | |
| poll_status=$(echo "$last_response" | jq -r '.data.attributes.status') | |
| [ "$poll_status" = "completed" ] && break | |
| done | |
| malicious=$(echo "$last_response" | jq -r '.data.attributes.stats.malicious // 0') | |
| suspicious=$(echo "$last_response" | jq -r '.data.attributes.stats.suspicious // 0') | |
| undetected=$(echo "$last_response" | jq -r '.data.attributes.stats.undetected // 0') | |
| harmless=$(echo "$last_response" | jq -r '.data.attributes.stats.harmless // 0') | |
| total=$((malicious + suspicious + undetected + harmless)) | |
| detected=$((malicious + suspicious)) | |
| [ "$detected" -eq 0 ] \ | |
| && result_status="✅ ${detected}/${total} Clean" \ | |
| || result_status="⚠️ ${detected}/${total} Detected" | |
| printf '%s' "https://www.virustotal.com/gui/file/$sha256/detection" > "${outprefix}.url" | |
| printf '%s' "$result_status" > "${outprefix}.status" | |
| } | |
| upload_and_poll "PlainApp-${VERSION}-default.apk" /tmp/vt_default & | |
| upload_and_poll "PlainApp-${VERSION}-armeabi-v7a.apk" /tmp/vt_armv7 & | |
| upload_and_poll "PlainApp-${VERSION}.aab" /tmp/vt_aab & | |
| wait | |
| { | |
| echo "default_report=$(cat /tmp/vt_default.url)" | |
| echo "default_status=$(cat /tmp/vt_default.status)" | |
| echo "armv7_report=$(cat /tmp/vt_armv7.url)" | |
| echo "armv7_status=$(cat /tmp/vt_armv7.status)" | |
| echo "aab_report=$(cat /tmp/vt_aab.url)" | |
| echo "aab_status=$(cat /tmp/vt_aab.status)" | |
| } >> $GITHUB_OUTPUT | |
| release: | |
| needs: [build, provenance, virustotal] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| name: release-artifacts | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| name: ${{ needs.provenance.outputs.provenance-name }} | |
| - name: Create GitHub Release | |
| uses: ncipollo/release-action@v1 | |
| with: | |
| tag: v${{ needs.build.outputs.version_name }} | |
| name: Release ${{ needs.build.outputs.version_name }} | |
| body: | | |
| ## What's Changed | |
| ## Security | |
| ### VirusTotal Scan | |
| | File | Status | Scan Report | | |
| |------|--------|-------------| | |
| | `PlainApp-${{ needs.build.outputs.version_name }}-default.apk` | ${{ needs.virustotal.outputs.default_status }} | [View Report](${{ needs.virustotal.outputs.default_report }}) | | |
| | `PlainApp-${{ needs.build.outputs.version_name }}-armeabi-v7a.apk` | ${{ needs.virustotal.outputs.armv7_status }} | [View Report](${{ needs.virustotal.outputs.armv7_report }}) | | |
| | `PlainApp-${{ needs.build.outputs.version_name }}.aab` | ${{ needs.virustotal.outputs.aab_status }} | [View Report](${{ needs.virustotal.outputs.aab_report }}) | | |
| ### SLSA Provenance (Level 3) | |
| The `.intoto.jsonl` file is a signed SLSA provenance document covering all release artifacts (APKs + AAB). | |
| Verify with [slsa-verifier](https://github.com/slsa-framework/slsa-verifier): | |
| ```sh | |
| slsa-verifier verify-artifact PlainApp-${{ needs.build.outputs.version_name }}-default.apk \ | |
| --provenance-path ${{ needs.provenance.outputs.provenance-name }} \ | |
| --source-uri github.com/${{ github.repository }} | |
| ``` | |
| draft: true | |
| prerelease: false | |
| artifacts: "PlainApp-${{ needs.build.outputs.version_name }}-default.apk,PlainApp-${{ needs.build.outputs.version_name }}-armeabi-v7a.apk,PlainApp-${{ needs.build.outputs.version_name }}.aab,${{ needs.provenance.outputs.provenance-name }}" |