[{"data":1,"prerenderedAt":88},["ShallowReactive",2],{"tutorial-cicd-per-flutter-build-firmate-e-pubblicazione-sugli-store-con-github-actions":3,"comments-tutorial-cicd-per-flutter-build-firmate-e-pubblicazione-sugli-store-con-github-actions":87},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"video":16,"difficulty":20,"estimated_minutes":21,"flutter_version":22,"status":23,"published_at":24,"meta_title":25,"meta_description":26,"category":27,"author":31,"steps":33},90,"CI\u002FCD per Flutter: build firmate e pubblicazione sugli store con GitHub Actions","cicd-per-flutter-build-firmate-e-pubblicazione-sugli-store-con-github-actions","Pipeline completa per Flutter: analisi statica, test, firma Android e iOS con segreti cifrati, build AAB\u002FIPA riproducibili e upload automatico su Google Play e TestFlight da GitHub Actions.","Automatizzare la consegna di un'app Flutter significa eliminare la classe di errori più fastidiosa: quella umana. Keystore dimenticati sul portatile di un collega, build number duplicati, provisioning profile scaduti, versioni caricate sullo store con il flavor sbagliato.\n\nIn questo tutorial **advanced** costruiamo una pipeline CI\u002FCD completa con **GitHub Actions**:\n\n- job di *quality gate* (`flutter analyze`, `dart format`, test unitari e widget) su ogni pull request;\n- **firma Android** con keystore iniettato dai GitHub Secrets in base64 e build di un `.aab` con obfuscation e symbol upload;\n- **firma iOS** con certificati e provisioning profile importati in un keychain temporaneo sul runner macOS;\n- **upload automatico** su Google Play (traccia `internal`) e su TestFlight tramite App Store Connect API key;\n- versionamento derivato dal **tag Git** e build number derivato da `github.run_number`, per build sempre riproducibili e monotone.\n\n> **Prerequisiti**: un progetto Flutter 3.x funzionante, un account Google Play Console con l'app già creata (il primo upload va sempre fatto a mano), un account Apple Developer con app registrata su App Store Connect, e diritti di amministratore sul repository per creare Secrets ed Environments.\n\nAlla fine avrai un flusso in cui `git tag v1.4.0 && git push --tags` produce una release firmata su entrambi gli store senza toccare un solo pulsante.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002Fb50c93ba-f770-41bf-80cb-76a8d336cc2b.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1737250386900-8487a3f0a891?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODkwMTQ3Mzh8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Wesley Derks","https:\u002F\u002Funsplash.com\u002F@derks21","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-group-of-pipes-sitting-next-to-each-other-UQxJ74tF-RE","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=eWMOUGSmH1M",{"youtube_id":17,"duration_seconds":18,"published_at":19},"eWMOUGSmH1M",164,"2026-09-10T09:44:08+00:00","advanced",75,"3.x","published","2026-09-10T04:32:19+00:00","CI\u002FCD Flutter con GitHub Actions: build firmate","Guida avanzata: pipeline GitHub Actions per Flutter con firma Android e iOS, secrets cifrati e pubblicazione automatica su Google Play e TestFlight.",{"id":28,"name":29,"slug":30},1,"Guide","guide",{"id":28,"name":32},"Flutter Bot",[34,42,50,58,65,72,80],{"id":35,"position":28,"title":36,"body":37,"code_snippet":38,"code_language":39,"expected_result":40,"demo_url":41,"video_url":41},598,"Impostare il quality gate: analisi, format e test su ogni PR","Prima di firmare qualsiasi cosa serve una pipeline che blocchi il codice rotto. Creiamo `.github\u002Fworkflows\u002Fci.yml`, eseguito su ogni pull request e su `main`.\n\nPunti chiave dell'approccio *advanced*:\n\n- **pin della versione di Flutter**: mai `channel: stable` senza versione, altrimenti la build di ieri non è riproducibile oggi. Se usi `fvm`, leggi la versione da `.fvmrc`\u002F`.fvm\u002Ffvm_config.json`.\n- **cache** delle dipendenze `pub` e degli artefatti Gradle: riduce i tempi da ~6 a ~2 minuti.\n- `dart format --set-exit-if-changed` e `flutter analyze --fatal-infos` per fallire su ogni deriva stilistica.\n- **concurrency group** per cancellare le run obsolete quando si pusha di nuovo sullo stesso branch.\n- coverage caricata su Codecov (opzionale ma utile come soglia di merge).\n\nAggiungi anche una regola di *branch protection* su `main` che richieda il job `quality` verde.","name: CI\n\non:\n  pull_request:\n    branches: [main, develop]\n  push:\n    branches: [main]\n\nconcurrency:\n  group: ci-${{ github.ref }}\n  cancel-in-progress: true\n\nenv:\n  FLUTTER_VERSION: '3.24.5'\n\njobs:\n  quality:\n    runs-on: ubuntu-latest\n    timeout-minutes: 20\n    steps:\n      - uses: actions\u002Fcheckout@v4\n\n      - uses: subosito\u002Fflutter-action@v2\n        with:\n          flutter-version: ${{ env.FLUTTER_VERSION }}\n          channel: stable\n          cache: true\n\n      - name: Dipendenze\n        run: flutter pub get\n\n      - name: Codegen (se usi build_runner)\n        run: dart run build_runner build --delete-conflicting-outputs\n        continue-on-error: false\n\n      - name: Formattazione\n        run: dart format --output=none --set-exit-if-changed .\n\n      - name: Analisi statica\n        run: flutter analyze --fatal-infos --fatal-warnings\n\n      - name: Test con coverage\n        run: flutter test --coverage --reporter expanded\n\n      - name: Upload coverage\n        uses: codecov\u002Fcodecov-action@v4\n        with:\n          files: coverage\u002Flcov.info\n          token: ${{ secrets.CODECOV_TOKEN }}\n        if: always()","yaml","Ogni pull request esegue automaticamente format, analyze e test; la run viene annullata se pushi un nuovo commit sullo stesso branch.",null,{"id":43,"position":44,"title":45,"body":46,"code_snippet":47,"code_language":48,"expected_result":49,"demo_url":41,"video_url":41},599,2,"Configurare la firma Android leggibile sia in locale sia in CI","La firma Android deve funzionare in due mondi: sul tuo Mac (dove hai `key.properties`) e sul runner (dove hai solo variabili d'ambiente). La soluzione robusta è un `build.gradle` che **legge prima il file, poi le variabili d'ambiente** e che, se non trova nulla, ricade sulla firma di debug solo per le build locali.\n\nGenera il keystore (una volta sola, poi conservalo in un password manager):\n\n```bash\nkeytool -genkey -v -keystore upload-keystore.jks \\\n  -keyalg RSA -keysize 2048 -validity 10000 -alias upload\n```\n\nAggiungi al `.gitignore`:\n\n```\nandroid\u002Fkey.properties\n**\u002F*.jks\n```\n\nNel `build.gradle` abilitiamo anche `minifyEnabled`\u002F`shrinkResources` e configuriamo `ndk.debugSymbolLevel` per caricare i simboli nativi su Play Console (crash report leggibili).","\u002F\u002F android\u002Fapp\u002Fbuild.gradle\n\ndef keystoreProperties = new Properties()\ndef keystorePropertiesFile = rootProject.file('key.properties')\nif (keystorePropertiesFile.exists()) {\n    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))\n}\n\n\u002F\u002F Fallback su variabili d'ambiente (CI)\ndef ksPath = keystoreProperties['storeFile'] ?: System.getenv('ANDROID_KEYSTORE_PATH')\ndef ksPassword = keystoreProperties['storePassword'] ?: System.getenv('ANDROID_KEYSTORE_PASSWORD')\ndef keyAlias = keystoreProperties['keyAlias'] ?: System.getenv('ANDROID_KEY_ALIAS')\ndef keyPassword = keystoreProperties['keyPassword'] ?: System.getenv('ANDROID_KEY_PASSWORD')\n\nandroid {\n    namespace \"it.example.myapp\"\n    compileSdk 34\n\n    defaultConfig {\n        applicationId \"it.example.myapp\"\n        minSdk 23\n        targetSdk 34\n        versionCode flutter.versionCode\n        versionName flutter.versionName\n        ndk {\n            debugSymbolLevel 'FULL'\n        }\n    }\n\n    signingConfigs {\n        release {\n            if (ksPath != null) {\n                storeFile file(ksPath)\n                storePassword ksPassword\n                keyAlias keyAlias\n                keyPassword keyPassword\n            }\n        }\n    }\n\n    buildTypes {\n        release {\n            signingConfig ksPath != null ? signingConfigs.release : signingConfigs.debug\n            minifyEnabled true\n            shrinkResources true\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n}","groovy","`flutter build appbundle --release` produce un AAB firmato con il keystore di upload sia in locale sia in CI, senza duplicare la configurazione.",{"id":51,"position":52,"title":53,"body":54,"code_snippet":55,"code_language":56,"expected_result":57,"demo_url":41,"video_url":41},600,3,"Cifrare e caricare i segreti su GitHub (Android e iOS)","I file binari (keystore, certificati `.p12`, provisioning profile) non possono stare in un Secret così come sono: vanno convertiti in **base64** su una sola riga.\n\n```bash\n# Android\nbase64 -i upload-keystore.jks | tr -d '\\n' | pbcopy   # macOS\nbase64 -w0 upload-keystore.jks                        # Linux\n\n# iOS: certificato di distribuzione esportato dal Portachiavi\nbase64 -i distribution.p12 | tr -d '\\n' | pbcopy\n# iOS: provisioning profile scaricato da developer.apple.com\nbase64 -i MyApp_AppStore.mobileprovision | tr -d '\\n' | pbcopy\n```\n\nPer Google Play crea un **service account** in Google Cloud, abilita l'API \"Google Play Android Developer\", genera una chiave JSON e concedile in Play Console (Utenti e autorizzazioni) il permesso di rilasciare sulle tracce di test.\n\nPer Apple crea in App Store Connect → Users and Access → Integrations una **API Key** (ruolo *App Manager*): otterrai `Issuer ID`, `Key ID` e il file `AuthKey_XXXX.p8`.\n\nVai poi in **Settings → Environments** del repo, crea l'environment `production` con *required reviewers* (così nessuno pubblica per sbaglio) e inserisci i Secret elencati nello snippet. Usare un Environment invece dei soli *repository secrets* ti dà approvazione manuale e log di audit.","# Secrets da creare nell'environment \"production\"\n#\n# --- Android ---\n# ANDROID_KEYSTORE_BASE64     -> contenuto base64 di upload-keystore.jks\n# ANDROID_KEYSTORE_PASSWORD\n# ANDROID_KEY_ALIAS\n# ANDROID_KEY_PASSWORD\n# PLAY_SERVICE_ACCOUNT_JSON   -> JSON del service account (testo integrale)\n#\n# --- iOS ---\n# IOS_CERT_P12_BASE64         -> certificato Apple Distribution in base64\n# IOS_CERT_PASSWORD           -> password di export del .p12\n# IOS_PROVISION_PROFILE_BASE64\n# IOS_KEYCHAIN_PASSWORD       -> stringa random, serve solo al runner\n# APPSTORE_ISSUER_ID\n# APPSTORE_KEY_ID\n# APPSTORE_PRIVATE_KEY        -> contenuto del file .p8 (con BEGIN\u002FEND)\n#\n# --- Comuni ---\n# API_BASE_URL, SENTRY_DSN ... -> passati via --dart-define\n\n# Verifica rapida che il base64 sia integro (deve stampare \"JKS\" o simile)\n# echo \"$ANDROID_KEYSTORE_BASE64\" | base64 -d > \u002Ftmp\u002Fks.jks && keytool -list -keystore \u002Ftmp\u002Fks.jks","bash","Tutti i segreti sono disponibili nell'environment `production`; nessun file sensibile è versionato nel repository.",{"id":59,"position":60,"title":61,"body":62,"code_snippet":63,"code_language":39,"expected_result":64,"demo_url":41,"video_url":41},601,4,"Workflow di release Android: build AAB firmato e upload su Google Play","Ora il cuore della pipeline. Il workflow si attiva su **tag** `v*` (oppure manualmente con `workflow_dispatch`) e:\n\n1. estrae la versione dal tag (`v1.4.0` → `1.4.0`);\n2. calcola il **build number** come `github.run_number` sommato a un offset, così è sempre crescente anche se cambi repo;\n3. decodifica il keystore in `android\u002Fapp\u002Fupload-keystore.jks` (fuori dal workspace tracciato);\n4. builda con `--obfuscate --split-debug-info` e carica i simboli come artefatto (ti serviranno per de-offuscare gli stack trace);\n5. pubblica sulla traccia `internal` con `r0adkll\u002Fupload-google-play`.\n\nNota il passo finale `rm -f`: cancellare esplicitamente i materiali di firma è buona igiene, anche se i runner GitHub-hosted sono effimeri. Attenzione anche a `--dart-define`: i valori compaiono nella riga di comando, quindi non passare mai chiavi *veramente* segrete al client (finirebbero comunque nel binario).","name: Release Android\n\non:\n  push:\n    tags: ['v*']\n  workflow_dispatch:\n\njobs:\n  android:\n    runs-on: ubuntu-latest\n    environment: production\n    timeout-minutes: 40\n    steps:\n      - uses: actions\u002Fcheckout@v4\n\n      - uses: actions\u002Fsetup-java@v4\n        with:\n          distribution: temurin\n          java-version: '17'\n          cache: gradle\n\n      - uses: subosito\u002Fflutter-action@v2\n        with:\n          flutter-version: '3.24.5'\n          channel: stable\n          cache: true\n\n      - name: Calcola versione e build number\n        id: ver\n        run: |\n          VERSION=\"${GITHUB_REF_NAME#v}\"\n          BUILD=$(( ${{ github.run_number }} + 1000 ))\n          echo \"version=$VERSION\" >> $GITHUB_OUTPUT\n          echo \"build=$BUILD\" >> $GITHUB_OUTPUT\n          echo \"Rilascio $VERSION+$BUILD\"\n\n      - name: Ripristina keystore\n        env:\n          KS: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}\n        run: echo \"$KS\" | base64 --decode > android\u002Fapp\u002Fupload-keystore.jks\n\n      - run: flutter pub get\n\n      - name: Build App Bundle firmato\n        env:\n          ANDROID_KEYSTORE_PATH: upload-keystore.jks\n          ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}\n          ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}\n          ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}\n        run: |\n          flutter build appbundle --release \\\n            --build-name=${{ steps.ver.outputs.version }} \\\n            --build-number=${{ steps.ver.outputs.build }} \\\n            --dart-define=API_BASE_URL=${{ vars.API_BASE_URL }} \\\n            --obfuscate --split-debug-info=build\u002Fsymbols\n\n      - name: Salva i simboli di debug\n        uses: actions\u002Fupload-artifact@v4\n        with:\n          name: debug-symbols-${{ steps.ver.outputs.version }}\n          path: build\u002Fsymbols\n          retention-days: 90\n\n      - name: Upload su Google Play (traccia internal)\n        uses: r0adkll\u002Fupload-google-play@v1\n        with:\n          serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}\n          packageName: it.example.myapp\n          releaseFiles: build\u002Fapp\u002Foutputs\u002Fbundle\u002Frelease\u002Fapp-release.aab\n          track: internal\n          status: completed\n          mappingFile: build\u002Fapp\u002Foutputs\u002Fmapping\u002Frelease\u002Fmapping.txt\n          debugSymbols: build\u002Fapp\u002Fintermediates\u002Fmerged_native_libs\u002Frelease\u002Fout\u002Flib\n\n      - name: Pulizia materiali di firma\n        if: always()\n        run: rm -f android\u002Fapp\u002Fupload-keystore.jks","Pushando il tag `v1.4.0`, dopo pochi minuti l'AAB firmato compare nella traccia interna di Google Play con versione 1.4.0 e build number crescente.",{"id":66,"position":67,"title":68,"body":69,"code_snippet":70,"code_language":39,"expected_result":71,"demo_url":41,"video_url":41},602,5,"Workflow di release iOS: keychain temporaneo, IPA e TestFlight","Su iOS la parte delicata è la firma. Sul runner macOS creiamo un **keychain temporaneo**, importiamo il `.p12`, installiamo il provisioning profile e usiamo `flutter build ipa` con un `ExportOptions.plist` in modalità `manual`.\n\nDue accortezze fondamentali:\n\n- `security set-key-partition-list` è indispensabile, altrimenti `codesign` resta bloccato in attesa di una conferma UI che non arriverà mai;\n- il **nome del provisioning profile** in `ExportOptions.plist` deve corrispondere esattamente a quello registrato su Apple Developer.\n\nPer l'upload usiamo `xcrun altool`\u002F`notarytool`? No: per TestFlight la via moderna è `xcrun altool --upload-app` con **API key** (niente password app-specific, niente 2FA da gestire). La chiave `.p8` va copiata in `~\u002Fprivate_keys` con il nome esatto `AuthKey_\u003CKEY_ID>.p8`.\n\nSe il progetto usa CocoaPods con dipendenze pesanti, aggiungi una cache di `ios\u002FPods` chiavata su `Podfile.lock`.","name: Release iOS\n\non:\n  push:\n    tags: ['v*']\n  workflow_dispatch:\n\njobs:\n  ios:\n    runs-on: macos-14\n    environment: production\n    timeout-minutes: 60\n    steps:\n      - uses: actions\u002Fcheckout@v4\n\n      - uses: subosito\u002Fflutter-action@v2\n        with:\n          flutter-version: '3.24.5'\n          channel: stable\n          cache: true\n\n      - name: Versione e build number\n        id: ver\n        run: |\n          echo \"version=${GITHUB_REF_NAME#v}\" >> $GITHUB_OUTPUT\n          echo \"build=$(( ${{ github.run_number }} + 1000 ))\" >> $GITHUB_OUTPUT\n\n      - name: Importa certificato e provisioning profile\n        env:\n          CERT: ${{ secrets.IOS_CERT_P12_BASE64 }}\n          CERT_PWD: ${{ secrets.IOS_CERT_PASSWORD }}\n          PROFILE: ${{ secrets.IOS_PROVISION_PROFILE_BASE64 }}\n          KC_PWD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}\n        run: |\n          CERT_PATH=$RUNNER_TEMP\u002Fcert.p12\n          PROFILE_PATH=$RUNNER_TEMP\u002Fprofile.mobileprovision\n          KEYCHAIN=$RUNNER_TEMP\u002Fbuild.keychain-db\n\n          echo \"$CERT\" | base64 --decode > \"$CERT_PATH\"\n          echo \"$PROFILE\" | base64 --decode > \"$PROFILE_PATH\"\n\n          security create-keychain -p \"$KC_PWD\" \"$KEYCHAIN\"\n          security set-keychain-settings -lut 21600 \"$KEYCHAIN\"\n          security unlock-keychain -p \"$KC_PWD\" \"$KEYCHAIN\"\n          security import \"$CERT_PATH\" -P \"$CERT_PWD\" -A -t cert -f pkcs12 -k \"$KEYCHAIN\"\n          security set-key-partition-list -S apple-tool:,apple: -k \"$KC_PWD\" \"$KEYCHAIN\"\n          security list-keychain -d user -s \"$KEYCHAIN\" login.keychain\n\n          mkdir -p ~\u002FLibrary\u002FMobileDevice\u002FProvisioning\\\\ Profiles\n          cp \"$PROFILE_PATH\" ~\u002FLibrary\u002FMobileDevice\u002FProvisioning\\\\ Profiles\u002F\n\n      - run: flutter pub get\n\n      - name: Genera ExportOptions.plist\n        run: |\n          cat > ios\u002FExportOptions.plist \u003C\u003C'PLIST'\n          \u003C?xml version=\"1.0\" encoding=\"UTF-8\"?>\n          \u003C!DOCTYPE plist PUBLIC \"-\u002F\u002FApple\u002F\u002FDTD PLIST 1.0\u002F\u002FEN\" \"http:\u002F\u002Fwww.apple.com\u002FDTDs\u002FPropertyList-1.0.dtd\">\n          \u003Cplist version=\"1.0\">\n          \u003Cdict>\n            \u003Ckey>method\u003C\u002Fkey>\u003Cstring>app-store\u003C\u002Fstring>\n            \u003Ckey>teamID\u003C\u002Fkey>\u003Cstring>ABCDE12345\u003C\u002Fstring>\n            \u003Ckey>signingStyle\u003C\u002Fkey>\u003Cstring>manual\u003C\u002Fstring>\n            \u003Ckey>uploadSymbols\u003C\u002Fkey>\u003Ctrue\u002F>\n            \u003Ckey>provisioningProfiles\u003C\u002Fkey>\n            \u003Cdict>\n              \u003Ckey>it.example.myapp\u003C\u002Fkey>\n              \u003Cstring>MyApp AppStore\u003C\u002Fstring>\n            \u003C\u002Fdict>\n          \u003C\u002Fdict>\n          \u003C\u002Fplist>\n          PLIST\n\n      - name: Build IPA\n        run: |\n          flutter build ipa --release \\\n            --build-name=${{ steps.ver.outputs.version }} \\\n            --build-number=${{ steps.ver.outputs.build }} \\\n            --dart-define=API_BASE_URL=${{ vars.API_BASE_URL }} \\\n            --obfuscate --split-debug-info=build\u002Fsymbols \\\n            --export-options-plist=ios\u002FExportOptions.plist\n\n      - name: Upload su TestFlight\n        env:\n          KEY_ID: ${{ secrets.APPSTORE_KEY_ID }}\n          ISSUER_ID: ${{ secrets.APPSTORE_ISSUER_ID }}\n          P8: ${{ secrets.APPSTORE_PRIVATE_KEY }}\n        run: |\n          mkdir -p ~\u002Fprivate_keys\n          echo \"$P8\" > ~\u002Fprivate_keys\u002FAuthKey_$KEY_ID.p8\n          IPA=$(ls build\u002Fios\u002Fipa\u002F*.ipa | head -n 1)\n          xcrun altool --upload-app -f \"$IPA\" -t ios \\\n            --apiKey \"$KEY_ID\" --apiIssuer \"$ISSUER_ID\"\n\n      - name: Pulizia keychain\n        if: always()\n        run: security delete-keychain $RUNNER_TEMP\u002Fbuild.keychain-db || true","L'IPA firmato viene caricato su App Store Connect e compare in TestFlight in stato \"Processing\" entro pochi minuti.",{"id":73,"position":74,"title":75,"body":76,"code_snippet":77,"code_language":78,"expected_result":79,"demo_url":41,"video_url":41},603,6,"Versionamento coerente e gestione dei flavor nella pipeline","Con due workflow separati rischi che Android e iOS abbiano numeri diversi. Centralizza la logica in uno script Dart eseguito da entrambi i job: legge il tag, valida il **SemVer**, verifica la coerenza con `pubspec.yaml` e scrive gli output per GitHub Actions.\n\nAggiungi anche il supporto ai flavor: se hai `dev`, `staging` e `prod`, il workflow di release deve accettare un input `flavor` e propagarlo a `flutter build appbundle --flavor prod -t lib\u002Fmain_prod.dart`. Ricorda che su iOS ogni flavor richiede uno **Scheme** Xcode e un provisioning profile dedicato: mappa quindi flavor → secret con una `include` nella matrice.\n\nLo script qui sotto fallisce (exit 1) se il tag non corrisponde alla versione dichiarata nel `pubspec.yaml`: è la guardia che impedisce di rilasciare `v1.4.0` con dentro `1.3.9`.","\u002F\u002F tool\u002Frelease_version.dart\n\u002F\u002F Uso: dart run tool\u002Frelease_version.dart v1.4.0 123\nimport 'dart:io';\n\nvoid main(List\u003CString> args) {\n  if (args.length \u003C 2) {\n    stderr.writeln('Uso: release_version.dart \u003Ctag> \u003CrunNumber>');\n    exit(64);\n  }\n\n  final tag = args[0];\n  final runNumber = int.parse(args[1]);\n\n  final semver = RegExp(r'^v(\\d+)\\.(\\d+)\\.(\\d+)$');\n  final match = semver.firstMatch(tag);\n  if (match == null) {\n    stderr.writeln('Tag non valido: $tag (atteso vX.Y.Z)');\n    exit(1);\n  }\n  final version = tag.substring(1);\n\n  final pubspec = File('pubspec.yaml').readAsStringSync();\n  final declared = RegExp(r'^version:\\s*([0-9.]+)', multiLine: true)\n      .firstMatch(pubspec)\n      ?.group(1);\n\n  if (declared != version) {\n    stderr.writeln('Mismatch: pubspec=$declared, tag=$version');\n    exit(1);\n  }\n\n  final buildNumber = runNumber + 1000;\n  final out = File(Platform.environment['GITHUB_OUTPUT'] ?? '\u002Fdev\u002Fstdout');\n  out.writeAsStringSync(\n    'version=$version\\nbuild=$buildNumber\\n',\n    mode: FileMode.append,\n  );\n  stdout.writeln('OK -> $version+$buildNumber');\n}","dart","Il job fallisce subito (in pochi secondi) se il tag e il `pubspec.yaml` divergono, evitando build inutili da 20 minuti.",{"id":81,"position":82,"title":83,"body":84,"code_snippet":85,"code_language":39,"expected_result":86,"demo_url":41,"video_url":41},604,7,"Rifinire la pipeline: approvazioni, notifiche, rollback e costi","Ultimo giro di vite per rendere la pipeline davvero *production ready*.\n\n**1. Approvazione umana.** L'environment `production` con *required reviewers* mette il job in pausa finché un maintainer non approva: è il tuo interruttore di sicurezza.\n\n**2. Release notes automatiche.** Genera il changelog dai commit tra due tag e passalo a Play Console come `whatsnew\u002Fwhatsnew-it-IT`, oppure crea la GitHub Release allegando AAB\u002FIPA.\n\n**3. Rollback.** Su Google Play non puoi \"cancellare\" una release: puoi però fermare il rollout graduale (`status: inProgress` + `userFraction: 0.1`) e promuovere solo dopo aver visto i crash-free users. Su TestFlight basta non promuovere la build.\n\n**4. Costi.** I runner macOS costano ~10x i runner Linux: esegui il job iOS **solo** su tag, mai su PR, e imposta sempre un `timeout-minutes` per evitare job appesi da 6 ore.\n\n**5. Sicurezza.** Usa `permissions: contents: read` di default, evita `pull_request_target`, pinna le action a un commit SHA se gestisci segreti di produzione, e ricorda che i log mascherano i secret solo se non li trasformi (un `base64` di un secret NON viene mascherato).\n\n**6. Debug delle build offuscate.** Conserva la cartella `build\u002Fsymbols` per ogni release: senza di essa gli stack trace di Crashlytics\u002FSentry sono illeggibili. Usa `flutter symbolize -i stack.txt -d symbols\u002Fapp.android-arm64.symbols`.","# Estratto: rollout graduale, changelog e notifica Slack\n\n      - name: Genera changelog dal tag precedente\n        id: changelog\n        run: |\n          PREV=$(git describe --tags --abbrev=0 HEAD^ 2>\u002Fdev\u002Fnull || echo \"\")\n          mkdir -p distribution\u002Fwhatsnew\n          if [ -z \"$PREV\" ]; then\n            git log --pretty=\"* %s\" -20 > distribution\u002Fwhatsnew\u002Fwhatsnew-it-IT\n          else\n            git log \"$PREV\"..HEAD --pretty=\"* %s\" > distribution\u002Fwhatsnew\u002Fwhatsnew-it-IT\n          fi\n          head -c 480 distribution\u002Fwhatsnew\u002Fwhatsnew-it-IT > \u002Ftmp\u002Fwn && mv \u002Ftmp\u002Fwn distribution\u002Fwhatsnew\u002Fwhatsnew-it-IT\n\n      - name: Upload con rollout al 10%\n        uses: r0adkll\u002Fupload-google-play@v1\n        with:\n          serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}\n          packageName: it.example.myapp\n          releaseFiles: build\u002Fapp\u002Foutputs\u002Fbundle\u002Frelease\u002Fapp-release.aab\n          track: production\n          status: inProgress\n          userFraction: 0.1\n          whatsNewDirectory: distribution\u002Fwhatsnew\n\n      - name: Notifica Slack\n        if: always()\n        uses: slackapi\u002Fslack-github-action@v1\n        with:\n          payload: |\n            {\"text\": \"Release ${{ github.ref_name }}: ${{ job.status }}\"}\n        env:\n          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}\n          SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK","La release richiede un'approvazione manuale, parte con un rollout al 10%, pubblica il changelog in italiano e notifica il team su Slack a fine job.",[],1789120575593]