[{"data":1,"prerenderedAt":82},["ShallowReactive",2],{"tutorial-release-automatizzate-in-un-monorepo-flutter-con-melos-versioning-changelog-e-publish-in-ci":3,"comments-tutorial-release-automatizzate-in-un-monorepo-flutter-con-melos-versioning-changelog-e-publish-in-ci":81},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"video":15,"difficulty":16,"estimated_minutes":17,"flutter_version":18,"status":19,"published_at":20,"meta_title":21,"meta_description":22,"category":23,"author":27,"steps":30},129,"Release automatizzate in un monorepo Flutter con Melos: versioning, changelog e publish in CI","release-automatizzate-in-un-monorepo-flutter-con-melos-versioning-changelog-e-publish-in-ci","Guida avanzata per trasformare un monorepo Flutter gestito con Melos in una pipeline di release ripetibile: conventional commits, versioning automatico dei package interni, changelog, tag Git e pubblicazione su pub.dev da GitHub Actions.","Quando un monorepo Flutter cresce oltre i tre o quattro package interni, il collo di bottiglia non è più il codice ma la **release**: chi alza la versione di `core_network`? Chi aggiorna i vincoli nei package che dipendono da esso? Chi scrive il changelog? Chi pubblica, e in che ordine?\n\nMelos risolve esattamente questo problema: analizza il grafo delle dipendenze interne, deduce il bump di versione dai **conventional commits**, aggiorna i vincoli nei package dipendenti, genera i `CHANGELOG.md`, crea un commit e i tag Git, e infine pubblica i package pubblicabili nell'ordine topologico corretto.\n\nIn questo tutorial costruiamo una pipeline di release completa su un monorepo realistico:\n\n```\nmy_app\u002F\n├── melos.yaml\n├── apps\u002F\n│   └── mobile\u002F            # app Flutter, non pubblicabile\n└── packages\u002F\n    ├── core_models\u002F       # modelli condivisi\n    ├── core_network\u002F      # dipende da core_models\n    └── design_system\u002F     # dipende da core_models\n```\n\nPartiamo dal presupposto che tu conosca già le basi di Melos (`melos bootstrap`, `melos exec`) e che il monorepo sia già in piedi: qui ci concentriamo su **versioning, changelog, tag e publish automatizzati**, inclusi i casi limite (hotfix, prerelease, rollback, breaking change prima della 1.0).\n\n> Riferimento: Melos 6.x con Dart 3.x. Se usi Melos 7 con i *pub workspaces* di Dart 3.6+, la configurazione `version`\u002F`publish` resta identica: cambia solo dove dichiari l'elenco dei package.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002Fa0a87a11-053f-4482-ae4c-58e3647fbe5d.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1538474705339-e87de81450e8?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTIzODQzNDR8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Samuel  Sianipar","https:\u002F\u002Funsplash.com\u002F@samthewam24","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fgrayscale-photography-of-metal-pipes-scUBcasSvbE",null,"advanced",55,"3.x (Dart 3.x, Melos 6.x)","published","2026-10-19T04:32:24+00:00","Release automatiche in un monorepo Flutter con Melos","Versioning, changelog, tag Git e publish su pub.dev automatizzati in un monorepo Flutter con Melos, conventional commits e GitHub Actions. Guida avanzata.",{"id":24,"name":25,"slug":26},3,"Best practice","best-practice",{"id":28,"name":29},1,"Flutter Bot",[31,38,45,51,59,66,73],{"id":32,"position":28,"title":33,"body":34,"code_snippet":35,"code_language":36,"expected_result":37,"demo_url":15,"video_url":15},861,"Configurare Melos per la release: package, script e pubspec_overrides","Il primo passo è definire la configurazione di base. Due punti sono critici per una release pulita:\n\n1. **Le dipendenze interne in `pubspec.yaml` devono essere dichiarate come dipendenze hosted con vincolo di versione**, non come `path:`. Un package con dipendenze `path:` non è pubblicabile su pub.dev. Melos risolve il problema in fase di `bootstrap` generando dei file `pubspec_overrides.yaml` (da mettere in `.gitignore`) che puntano ai path locali: in sviluppo lavori sul codice locale, in pubblicazione il vincolo pubblicato è quello corretto.\n2. **Gli script devono essere eseguibili sia in locale sia in CI**, perché li richiameremo dagli hook di versioning.\n\nCrea quindi `melos.yaml` alla radice e verifica che il grafo sia quello atteso con `melos list --graph`.","# melos.yaml\nname: my_app\nrepository: https:\u002F\u002Fgithub.com\u002Facme\u002Fmy_app\n\npackages:\n  - apps\u002F**\n  - packages\u002F**\n\ncommand:\n  bootstrap:\n    # i vincoli condivisi stanno qui: un solo punto di verità\n    environment:\n      sdk: \">=3.4.0 \u003C4.0.0\"\n      flutter: \">=3.22.0\"\n    dependencies:\n      collection: ^1.18.0\n    dev_dependencies:\n      very_good_analysis: ^6.0.0\n    hooks:\n      post: melos run generate\n\nscripts:\n  analyze:\n    run: melos exec -c 1 -- dart analyze . --fatal-infos --fatal-warnings\n    description: Analisi statica su tutti i package.\n\n  test:\n    run: melos exec -c 4 --dir-exists=test -- flutter test\n    description: Test di tutti i package che hanno una cartella test.\n\n  generate:\n    run: melos exec -c 1 --depends-on=build_runner -- dart run build_runner build --delete-conflicting-outputs\n    description: Rigenera il codice (json_serializable, freezed, ...).\n\n  publish:check:\n    run: melos publish --dry-run --no-private --yes\n    description: Simula la pubblicazione senza toccare pub.dev.","yaml","`melos bootstrap` collega i package locali tramite `pubspec_overrides.yaml` e `melos list --graph` mostra il grafo `core_models -> core_network\u002Fdesign_system`. I `pubspec.yaml` contengono solo vincoli hosted.",{"id":39,"position":40,"title":41,"body":42,"code_snippet":43,"code_language":36,"expected_result":44,"demo_url":15,"video_url":15},862,2,"Adottare i conventional commits e configurare `command\u002Fversion`","Melos deduce il bump di versione dai messaggi di commit secondo la convenzione **Conventional Commits**:\n\n| Prefisso | Effetto sulla versione |\n|---|---|\n| `fix:` | patch (`1.2.3` → `1.2.4`) |\n| `feat:` | minor (`1.2.3` → `1.3.0`) |\n| `feat!:` oppure footer `BREAKING CHANGE:` | major (`1.2.3` → `2.0.0`), **ma** prima della 1.0.0 diventa un bump minor |\n| `chore:`, `docs:`, `refactor:`, `test:` | nessun bump (compaiono comunque nel changelog se configurati) |\n\nIl punto fondamentale è lo **scope**: Melos associa il commit a un package attraverso i file modificati, ma usare lo scope (`feat(core_network): ...`) rende il changelog molto più leggibile e permette di filtrare le sezioni.\n\nAggiungi la sezione `command\u002Fversion` a `melos.yaml`. Le opzioni chiave:\n\n- `branch`: impedisce di versionare da un branch sbagliato;\n- `linkToCommits`: inserisce nel changelog i link ai commit (richiede `repository` configurato);\n- `workspaceChangelog`: genera un `CHANGELOG.md` riepilogativo alla radice;\n- `updateGitTagRefs`: aggiorna i riferimenti ai tag nei file (utile per badge e doc);\n- `changelogs`: changelog aggiuntivi filtrati per scope.\n\nPer rendere la convenzione *obbligatoria* e non solo *consigliata*, aggiungi una validazione in CI sui titoli delle PR (es. `amannn\u002Faction-semantic-pull-request`) oppure un hook `commit-msg` locale.","# melos.yaml (estratto)\ncommand:\n  version:\n    # versiona solo da main: evita tag creati da feature branch\n    branch: main\n    message: \"chore(release): publish packages\\n\\n{new_package_versions}\"\n    includeCommitId: true\n    linkToCommits: true\n    workspaceChangelog: true\n    updateGitTagRefs: true\n    # propaga il bump ai package dipendenti e aggiorna i loro vincoli\n    includeScopes: true\n    changelogs:\n      - path: CHANGELOG.md\n        description: |\n          Changelog consolidato di tutti i package pubblici del monorepo.\n        packageFilters:\n          noPrivate: true\n    aggregateChangelogs: true\n    hooks:\n      preCommit: melos run generate && git add .","Un commit `feat(core_network): aggiunge retry configurabile` viene riconosciuto da Melos come bump minor del package `core_network`.",{"id":46,"position":24,"title":47,"body":48,"code_snippet":49,"code_language":36,"expected_result":50,"demo_url":15,"video_url":15},863,"Blindare la release con hook e quality gate","Un tag di release deve corrispondere a un commit **verde**: se il codice generato non è aggiornato o i test falliscono, il tag è già marcio. Melos espone hook sul comando `version` che possiamo usare come quality gate.\n\nHook disponibili sul comando `version`:\n\n- `preCommit`: eseguito dopo l'aggiornamento di versioni e changelog, **prima** del commit di release. È il punto giusto per rigenerare codice che dipende dalla versione (es. costanti `appVersion`) e fare `git add`.\n- `post`: eseguito a versioning completato.\n\nL'analisi statica e i test, invece, vanno eseguiti **prima** di avviare il versioning: li incapsuliamo in uno script `release:prepare` che fa da porta d'ingresso unica, sia in locale sia in CI.\n\nUn dettaglio spesso trascurato: se un package espone una costante con la propria versione, generala da `pubspec.yaml` nell'hook `preCommit`, altrimenti resterà disallineata rispetto al tag.","# melos.yaml (estratto)\nscripts:\n  release:prepare:\n    run: |\n      melos bootstrap && \\\n      melos run generate && \\\n      melos run analyze && \\\n      melos run test && \\\n      melos run publish:check\n    description: Quality gate completo prima di versionare.\n\n  version:sync:\n    run: melos exec -c 1 --no-private -- dart run ..\u002F..\u002Ftool\u002Fwrite_version.dart\n    description: Scrive lib\u002Fsrc\u002Fversion.g.dart leggendo la versione dal pubspec.\n\ncommand:\n  version:\n    hooks:\n      preCommit: melos run version:sync && git add .","`melos run release:prepare` fallisce (exit code ≠ 0) se analisi, test o dry-run della pubblicazione non passano, bloccando la release prima che venga creato qualsiasi tag.",{"id":52,"position":53,"title":54,"body":55,"code_snippet":56,"code_language":57,"expected_result":58,"demo_url":15,"video_url":15},864,4,"Versionare in locale: graduate, prerelease e versioni manuali","Ora la parte centrale. `melos version` esegue, in un'unica transazione:\n\n1. calcolo del bump per ogni package modificato dall'ultimo tag;\n2. **propagazione ai dipendenti**: se `core_models` passa a `2.0.0`, i vincoli `core_models: ^1.x` in `core_network` e `design_system` vengono riscritti e quei package ricevono un bump patch;\n3. scrittura dei `CHANGELOG.md`;\n4. commit di release + un tag per package nel formato `nome_package-vX.Y.Z`.\n\nFlag che userai davvero:\n\n- `--yes`: non interattivo (obbligatorio in CI);\n- `--no-private`: ignora i package con `publish_to: none` (tipicamente le app);\n- `--prerelease --preid=beta`: produce `1.3.0-beta.0`;\n- `--graduate`: promuove una prerelease a versione stabile (`1.3.0-beta.3` → `1.3.0`);\n- `-V core_network:2.0.0`: forza una versione manuale per un package specifico;\n- `--dependent-versions` \u002F `--dependent-constraints`: controllano la propagazione (per default entrambi attivi);\n- `--no-git-tag-version`: calcola e scrive versioni e changelog senza creare commit e tag (utile per ispezionare il diff).\n\n**Consiglio operativo**: la prima volta esegui con `--no-git-tag-version` su un branch usa e getta, ispeziona il diff, poi `git checkout .`.\n\nAttenzione al semver pre-1.0: con versione `0.x.y` un `BREAKING CHANGE` produce `0.(x+1).0`, non `1.0.0`. Se vuoi davvero la 1.0.0, usa `-V package:1.0.0`.","# 1) Prova a vuoto: scrive versioni e changelog senza commit né tag\nmelos version --no-private --no-git-tag-version --yes\ngit diff            # ispeziona il risultato\ngit checkout .      # annulla\n\n# 2) Release stabile reale\nmelos run release:prepare\nmelos version --no-private --yes\n\n# 3) Prerelease beta su un branch di integrazione\nmelos version --no-private --prerelease --preid=beta --yes\n\n# 4) Promozione delle beta a stabile\nmelos version --no-private --graduate --yes\n\n# 5) Forzare una major su un package specifico\nmelos version -V core_models:2.0.0 --yes\n\n# 6) Pubblica il commit di release e TUTTI i tag generati\ngit push --follow-tags origin main","bash","Un commit `chore(release): publish packages` con i `CHANGELOG.md` aggiornati, i `pubspec.yaml` modificati (versione + vincoli interni riallineati) e un tag Git per ciascun package versionato.",{"id":60,"position":61,"title":62,"body":63,"code_snippet":64,"code_language":36,"expected_result":65,"demo_url":15,"video_url":15},865,5,"Preparare e verificare la pubblicazione su pub.dev","Prima di pubblicare, ogni package pubblico deve superare una checklist:\n\n- `publish_to: none` **solo** su app e package interni non destinati a pub.dev (Melos li esclude con `--no-private`);\n- nessuna dipendenza `path:` o `git:` nel `pubspec.yaml` versionato (i `pubspec_overrides.yaml` devono essere in `.gitignore`);\n- `homepage`\u002F`repository` e `description` valorizzati, `LICENSE`, `README.md` e `CHANGELOG.md` presenti;\n- vincoli SDK coerenti tra i package del monorepo.\n\n`melos publish` esegue per default un **dry-run** e, aspetto cruciale, calcola l'ordine topologico: `core_models` viene pubblicato prima di `core_network`. Pubblica solo i package la cui versione non è ancora presente su pub.dev, quindi è idempotente: se la CI va in errore a metà, puoi rilanciarla senza duplicare pubblicazioni.\n\nPer pubblicare davvero serve `--no-dry-run`. Le credenziali possono arrivare in due modi:\n\n- **OIDC di GitHub Actions** (consigliato): su pub.dev abiliti \"Automated publishing\" per ciascun package, indicando repository e pattern dei tag (es. `{{package}}-v{{version}}`, che è esattamente il formato dei tag di Melos). Nessun segreto da gestire.\n- **Credenziali statiche**: scrivi il contenuto di `~\u002F.config\u002Fdart\u002Fpub-credentials.json` in un secret e ricostruisci il file in CI. Più semplice, ma rotazione a tuo carico.","# packages\u002Fcore_network\u002Fpubspec.yaml\nname: core_network\ndescription: Client HTTP condiviso con retry, interceptor e mapping degli errori.\nversion: 1.3.0\nrepository: https:\u002F\u002Fgithub.com\u002Facme\u002Fmy_app\u002Ftree\u002Fmain\u002Fpackages\u002Fcore_network\nhomepage: https:\u002F\u002Fgithub.com\u002Facme\u002Fmy_app\n\nenvironment:\n  sdk: \">=3.4.0 \u003C4.0.0\"\n\ndependencies:\n  core_models: ^2.0.0   # vincolo hosted: NON usare path qui\n  dio: ^5.4.0\n\n# apps\u002Fmobile\u002Fpubspec.yaml\nname: mobile\npublish_to: none        # escluso da --no-private\nversion: 1.8.0+42","`melos publish --dry-run --no-private --yes` elenca i package da pubblicare nell'ordine corretto e non segnala errori di validazione (`Package has 0 warnings`).",{"id":67,"position":68,"title":69,"body":70,"code_snippet":71,"code_language":36,"expected_result":72,"demo_url":15,"video_url":15},866,6,"Automatizzare la release con GitHub Actions","Mettiamo insieme i pezzi in due workflow distinti, perché hanno permessi e trigger diversi:\n\n**Workflow 1 — `release.yml` (manuale, `workflow_dispatch`)**: esegue il quality gate, lancia `melos version` e pusha commit e tag. Serve `contents: write`. Il bot deve avere un'identità Git configurata.\n\n**Workflow 2 — `publish.yml` (trigger sui tag)**: parte quando arrivano i tag creati dal primo workflow e pubblica su pub.dev usando OIDC. Serve `id-token: write`.\n\nPerché separarli? Perché il push dei tag da parte di un workflow con `GITHUB_TOKEN` **non innesca** altri workflow. La soluzione pulita è pubblicare dal medesimo job (come nell'esempio sotto, con un job dipendente) oppure usare un PAT\u002FGitHub App per il push. Qui usiamo un unico workflow con due job collegati da `needs`, così evitiamo il problema alla radice.\n\nNota sul `fetch-depth: 0`: Melos ha bisogno dell'intera storia e di **tutti i tag** per capire qual è stata l'ultima release di ogni package. Con un checkout shallow calcolerebbe bump sbagliati.","# .github\u002Fworkflows\u002Frelease.yml\nname: release\n\non:\n  workflow_dispatch:\n    inputs:\n      prerelease:\n        description: \"Pubblica come prerelease beta\"\n        type: boolean\n        default: false\n\npermissions:\n  contents: write\n  id-token: write   # necessario per il publishing OIDC su pub.dev\n\njobs:\n  version:\n    runs-on: ubuntu-latest\n    outputs:\n      sha: ${{ steps.push.outputs.sha }}\n    steps:\n      - uses: actions\u002Fcheckout@v4\n        with:\n          fetch-depth: 0        # storia + tag completi: indispensabile\n          ref: main\n\n      - uses: subosito\u002Fflutter-action@v2\n        with:\n          flutter-version: '3.24.x'\n          cache: true\n\n      - name: Install Melos\n        run: dart pub global activate melos 6.2.0\n\n      - name: Bootstrap\n        run: melos bootstrap\n\n      - name: Quality gate\n        run: melos run release:prepare\n\n      - name: Configure git bot\n        run: |\n          git config user.name \"release-bot\"\n          git config user.email \"release-bot@users.noreply.github.com\"\n\n      - name: Version packages\n        run: |\n          if [ \"${{ inputs.prerelease }}\" = \"true\" ]; then\n            melos version --no-private --prerelease --preid=beta --yes\n          else\n            melos version --no-private --yes\n          fi\n\n      - name: Push commit and tags\n        id: push\n        run: |\n          git push --follow-tags origin main\n          echo \"sha=$(git rev-parse HEAD)\" >> \"$GITHUB_OUTPUT\"\n\n  publish:\n    needs: version\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      id-token: write\n    steps:\n      - uses: actions\u002Fcheckout@v4\n        with:\n          fetch-depth: 0\n          ref: ${{ needs.version.outputs.sha }}\n\n      - uses: subosito\u002Fflutter-action@v2\n        with:\n          flutter-version: '3.24.x'\n          cache: true\n\n      - run: dart pub global activate melos 6.2.0\n      - run: melos bootstrap\n\n      # Rimuove gli override locali: su pub.dev devono valere i vincoli hosted\n      - name: Clean pubspec overrides\n        run: find . -name pubspec_overrides.yaml -delete\n\n      - name: Publish to pub.dev\n        run: melos publish --no-dry-run --no-private --yes","Avviando il workflow da GitHub, il job `version` crea commit e tag su `main` e il job `publish` carica su pub.dev solo i package con versioni non ancora pubblicate, nell'ordine topologico corretto.",{"id":74,"position":75,"title":76,"body":77,"code_snippet":78,"code_language":79,"expected_result":80,"demo_url":15,"video_url":15},867,7,"Gestire i casi limite: hotfix, rollback e package che non devono finire su pub.dev","La pipeline funziona: ora servono le procedure per quando qualcosa va storto.\n\n**Hotfix su una release passata.** Crea un branch dal tag (`git checkout -b hotfix\u002Fcore_network-1.3.x core_network-v1.3.0`), applica il `fix:`, poi versiona forzando il branch consentito e la versione:\n\n```bash\nmelos version -V core_network:1.3.1 --yes --no-private\n```\n\nRicorda che `command\u002Fversion\u002Fbranch: main` blocca il versioning da altri branch: usa `melos version --no-private --yes` dopo aver temporaneamente passato `--branch hotfix\u002F...` oppure sposta il controllo in CI.\n\n**Rollback.** Su pub.dev non si può cancellare una versione: puoi solo **ritirarla** (*retract*, entro 7 giorni) dalla dashboard del package e pubblicare subito una patch correttiva. In locale, se il tag non è ancora stato pushato:\n\n```bash\ngit tag -d core_network-v1.3.1\ngit reset --hard HEAD~1\n```\n\n**Package interni mai pubblicabili.** Oltre a `publish_to: none`, aggiungi una verifica automatica in CI che fallisca se un `pubspec.yaml` pubblico contiene dipendenze `path:` o `git:`: è l'errore che più spesso rompe una release.\n\n**Versioni allineate tra più package.** Se vuoi che un gruppo di package condivida sempre la stessa versione (modello *fixed versioning*), usa `-V` multipli nello stesso comando: Melos li applica in una sola transazione, mantenendo coerenti vincoli e changelog.\n\n**Verifica finale.** Dopo ogni release, controlla che l'app consumi davvero le versioni pubblicate: rimuovi gli override e lancia `flutter pub get` in `apps\u002Fmobile` partendo da un checkout pulito del tag.","\u002F\u002F tool\u002Fcheck_publishable.dart\n\u002F\u002F Fallisce se un package pubblico dichiara dipendenze path: o git:.\nimport 'dart:io';\nimport 'package:yaml\u002Fyaml.dart';\n\nvoid main() {\n  final failures = \u003CString>[];\n\n  for (final entity in Directory('packages').listSync()) {\n    if (entity is! Directory) continue;\n    final file = File('${entity.path}\u002Fpubspec.yaml');\n    if (!file.existsSync()) continue;\n\n    final yaml = loadYaml(file.readAsStringSync()) as YamlMap;\n    if (yaml['publish_to'] == 'none') continue;\n\n    final name = yaml['name'] as String;\n    for (final key in const ['dependencies', 'dev_dependencies']) {\n      final deps = yaml[key];\n      if (deps is! YamlMap) continue;\n      deps.forEach((dep, spec) {\n        if (spec is YamlMap && (spec.containsKey('path') || spec.containsKey('git'))) {\n          failures.add('$name -> $dep usa una dipendenza non pubblicabile ($key)');\n        }\n      });\n    }\n  }\n\n  if (failures.isNotEmpty) {\n    stderr.writeln('Package non pubblicabili:\\n - ${failures.join('\\n - ')}');\n    exit(1);\n  }\n  stdout.writeln('Tutti i package pubblici sono pubblicabili.');\n}","dart","`dart run tool\u002Fcheck_publishable.dart` termina con exit code 0 su un monorepo sano e blocca la pipeline non appena qualcuno reintroduce una dipendenza `path:` in un package destinato a pub.dev.",[],1792392188401]