From fe1e5b2685a4bc54662b0621a51674e4c20b3d44 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 14 Sep 2026 09:55:04 +0200 Subject: [PATCH] feat(ci): build only 3 split APKs, upload individual artifacts, and publish unzipped APKs to Gitea Releases --- .gitea/workflows/build-apk.yaml | 50 ++++++++------ .github/workflows/build-apk.yaml | 42 ++++++------ scripts/ci/publish_release.js | 108 +++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 39 deletions(-) create mode 100644 scripts/ci/publish_release.js diff --git a/.gitea/workflows/build-apk.yaml b/.gitea/workflows/build-apk.yaml index 76e7a4a..f6a4946 100644 --- a/.gitea/workflows/build-apk.yaml +++ b/.gitea/workflows/build-apk.yaml @@ -18,6 +18,8 @@ jobs: build-android: name: Build Release APKs runs-on: ubuntu-latest + permissions: + contents: write container: image: ghcr.io/cirruslabs/flutter:3.19.3 options: --privileged @@ -100,14 +102,7 @@ jobs: flutter analyze flutter test - - name: Build Universal Release APK - run: | - flutter build apk --release || { - echo "Checking if APK was built despite path mismatch..." - find build android -type f -name "*release*.apk" 2>/dev/null - } - - - name: Build Split-per-ABI Release APKs + - name: Build Split-per-ABI Release APKs (3 APKs) run: | flutter build apk --release --split-per-abi || { echo "Checking if split APKs were built despite path mismatch..." @@ -117,13 +112,6 @@ jobs: - name: Organize Artifacts run: | mkdir -p release-apks - # Copy universal APK - if [ -f build/app/outputs/flutter-apk/app-release.apk ]; then - cp build/app/outputs/flutter-apk/app-release.apk release-apks/oggauer-gemeindetrommler-universal-release.apk - elif [ -f android/app/build/outputs/apk/release/app-release.apk ]; then - cp android/app/build/outputs/apk/release/app-release.apk release-apks/oggauer-gemeindetrommler-universal-release.apk - fi - # Copy split APKs for apk in build/app/outputs/flutter-apk/*.apk android/app/build/outputs/apk/release/*.apk; do [ -f "$apk" ] || continue case "$(basename "$apk")" in @@ -132,12 +120,36 @@ jobs: *x86_64*) cp "$apk" release-apks/oggauer-gemeindetrommler-x86_64.apk ;; esac done - echo "=== Generated APK Artifacts ===" + echo "=== Generated 3 Split APK Artifacts ===" ls -lah release-apks/ - - name: Upload APKs to Gitea Artifacts + - name: Upload ARM64 APK uses: actions/upload-artifact@v3 with: - name: oggauer-gemeindetrommler-apks - path: release-apks/*.apk + name: oggauer-gemeindetrommler-arm64-v8a + path: release-apks/oggauer-gemeindetrommler-arm64-v8a.apk retention-days: 30 + + - name: Upload ARMv7 APK + uses: actions/upload-artifact@v3 + with: + name: oggauer-gemeindetrommler-armeabi-v7a + path: release-apks/oggauer-gemeindetrommler-armeabi-v7a.apk + retention-days: 30 + + - name: Upload x86_64 APK + uses: actions/upload-artifact@v3 + with: + name: oggauer-gemeindetrommler-x86_64 + path: release-apks/oggauer-gemeindetrommler-x86_64.apk + retention-days: 30 + + - name: Publish Direct APKs to Gitea Releases + continue-on-error: true + env: + GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITEA_API: ${{ github.api_url }} + REPO: ${{ github.repository }} + RELEASE_TAG: latest + run: | + node scripts/ci/publish_release.js diff --git a/.github/workflows/build-apk.yaml b/.github/workflows/build-apk.yaml index 182604d..2e69342 100644 --- a/.github/workflows/build-apk.yaml +++ b/.github/workflows/build-apk.yaml @@ -18,6 +18,8 @@ jobs: build-android: name: Build Release APKs runs-on: ubuntu-latest + permissions: + contents: write container: image: ghcr.io/cirruslabs/flutter:3.19.3 options: --privileged @@ -100,14 +102,7 @@ jobs: flutter analyze flutter test - - name: Build Universal Release APK - run: | - flutter build apk --release || { - echo "Checking if APK was built despite path mismatch..." - find build android -type f -name "*release*.apk" 2>/dev/null - } - - - name: Build Split-per-ABI Release APKs + - name: Build Split-per-ABI Release APKs (3 APKs) run: | flutter build apk --release --split-per-abi || { echo "Checking if split APKs were built despite path mismatch..." @@ -117,13 +112,6 @@ jobs: - name: Organize Artifacts run: | mkdir -p release-apks - # Copy universal APK - if [ -f build/app/outputs/flutter-apk/app-release.apk ]; then - cp build/app/outputs/flutter-apk/app-release.apk release-apks/oggauer-gemeindetrommler-universal-release.apk - elif [ -f android/app/build/outputs/apk/release/app-release.apk ]; then - cp android/app/build/outputs/apk/release/app-release.apk release-apks/oggauer-gemeindetrommler-universal-release.apk - fi - # Copy split APKs for apk in build/app/outputs/flutter-apk/*.apk android/app/build/outputs/apk/release/*.apk; do [ -f "$apk" ] || continue case "$(basename "$apk")" in @@ -132,12 +120,26 @@ jobs: *x86_64*) cp "$apk" release-apks/oggauer-gemeindetrommler-x86_64.apk ;; esac done - echo "=== Generated APK Artifacts ===" + echo "=== Generated 3 Split APK Artifacts ===" ls -lah release-apks/ - - name: Upload APKs to Gitea Artifacts - uses: actions/upload-artifact@v4 + - name: Upload ARM64 APK + uses: actions/upload-artifact@v3 with: - name: oggauer-gemeindetrommler-apks - path: release-apks/*.apk + name: oggauer-gemeindetrommler-arm64-v8a + path: release-apks/oggauer-gemeindetrommler-arm64-v8a.apk + retention-days: 30 + + - name: Upload ARMv7 APK + uses: actions/upload-artifact@v3 + with: + name: oggauer-gemeindetrommler-armeabi-v7a + path: release-apks/oggauer-gemeindetrommler-armeabi-v7a.apk + retention-days: 30 + + - name: Upload x86_64 APK + uses: actions/upload-artifact@v3 + with: + name: oggauer-gemeindetrommler-x86_64 + path: release-apks/oggauer-gemeindetrommler-x86_64.apk retention-days: 30 diff --git a/scripts/ci/publish_release.js b/scripts/ci/publish_release.js new file mode 100644 index 0000000..45f0b70 --- /dev/null +++ b/scripts/ci/publish_release.js @@ -0,0 +1,108 @@ +const fs = require('fs'); +const path = require('path'); + +async function main() { + const token = process.env.GITEA_TOKEN; + const repo = process.env.REPO; + let apiUrl = (process.env.GITEA_API || 'https://git.djnizz.at/api/v1').replace(/\/$/, ''); + if (!apiUrl.includes('/api/v1')) { + apiUrl += '/api/v1'; + } + const tag = process.env.RELEASE_TAG || 'latest'; + const apksDir = path.resolve('release-apks'); + + if (!token) { + console.log('No GITEA_TOKEN provided, skipping direct release publishing.'); + return; + } + if (!fs.existsSync(apksDir)) { + console.log('No release-apks directory found, skipping.'); + return; + } + + const apkFiles = fs.readdirSync(apksDir).filter(f => f.endsWith('.apk')); + if (apkFiles.length === 0) { + console.log('No APK files found in release-apks, skipping.'); + return; + } + + console.log(`Publishing ${apkFiles.length} APK(s) to Gitea release tag '${tag}'...`); + + // 1. Get or create release + let release = null; + const tagRes = await fetch(`${apiUrl}/repos/${repo}/releases/tags/${tag}`, { + headers: { 'Authorization': `token ${token}` } + }); + + if (tagRes.ok) { + release = await tagRes.json(); + console.log(`Found existing release (ID: ${release.id}) for tag '${tag}'.`); + } else { + console.log(`Release for tag '${tag}' does not exist, creating new release...`); + const createRes = await fetch(`${apiUrl}/repos/${repo}/releases`, { + method: 'POST', + headers: { + 'Authorization': `token ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + tag_name: tag, + name: `Oggauer Gemeindetrommler - ${tag.toUpperCase()}`, + body: `Automatische Builds der Android APKs (Split per ABI).\nDirekte APK-Downloads ohne ZIP.`, + draft: false, + prerelease: false + }) + }); + if (!createRes.ok) { + console.warn(`Could not create release: ${createRes.status} ${await createRes.text()}`); + return; + } + release = await createRes.json(); + console.log(`Created release with ID ${release.id}.`); + } + + // 2. Query existing assets to delete duplicates + const assetsRes = await fetch(`${apiUrl}/repos/${repo}/releases/${release.id}/assets`, { + headers: { 'Authorization': `token ${token}` } + }); + const existingAssets = assetsRes.ok ? await assetsRes.json() : []; + + // 3. Upload each APK + for (const fileName of apkFiles) { + const filePath = path.join(apksDir, fileName); + const existing = existingAssets.find(a => a.name === fileName); + if (existing) { + console.log(`Deleting old asset ${fileName} (ID: ${existing.id})...`); + await fetch(`${apiUrl}/repos/${repo}/releases/${release.id}/assets/${existing.id}`, { + method: 'DELETE', + headers: { 'Authorization': `token ${token}` } + }); + } + + console.log(`Uploading ${fileName} (${(fs.statSync(filePath).size / (1024 * 1024)).toFixed(1)} MB)...`); + const fileBytes = fs.readFileSync(filePath); + const form = new FormData(); + form.append('attachment', new Blob([fileBytes]), fileName); + + const uploadRes = await fetch(`${apiUrl}/repos/${repo}/releases/${release.id}/assets?name=${encodeURIComponent(fileName)}`, { + method: 'POST', + headers: { + 'Authorization': `token ${token}` + }, + body: form + }); + + if (uploadRes.ok) { + const asset = await uploadRes.json(); + console.log(`✓ Uploaded ${fileName}: ${asset.browser_download_url || fileName}`); + } else { + console.warn(`Failed to upload ${fileName}: ${uploadRes.status} ${await uploadRes.text()}`); + } + } + + console.log('Finished publishing APKs to Gitea Releases!'); +} + +main().catch(err => { + console.error('Error in publish_release.js:', err); +});