GitHub Actions vs GitLab CI per backend, web e mobile
Confronto tra GitHub Actions e GitLab CI per monorepo: setup dei runner, gestione dei segreti, caching e pattern pratici di pipeline per backend, web e mobile.

Cosa crea problemi nella CI multi-app
Quando un repository contiene backend, app web e app mobile, la CI smette di essere "solo eseguire i test". Diventa un controllore del traffico per toolchain diverse, tempi di build diversi e regole di rilascio diverse.
Il problema più comune è semplice: una piccola modifica scatena troppo lavoro. Una modifica alla documentazione innesca la firma iOS, o una variazione al backend forza una ricostruzione completa del web, e all'improvviso ogni merge sembra lento e rischioso.
In setup multi-app emergono presto alcuni problemi:
- Runner drift: le versioni degli SDK differiscono tra macchine, quindi le build si comportano diversamente tra CI e locale.
- Segreti che si moltiplicano: API key, certificati di firma e credenziali degli store vengono duplicati tra job e ambienti.
- Confusione sulla cache: la chiave sbagliata genera build stale, ma l'assenza di cache rende tutto dolorosamente lento.
- Regole di rilascio miste: i backend vogliono deploy frequenti, mentre le release mobile sono soggette a gating e controlli extra.
- Leggibilità della pipeline: la configurazione cresce fino a diventare un muro di job che nessuno vuole toccare.
Ecco perché la scelta tra GitHub Actions e GitLab CI conta di più nei monorepo che nei progetti single-app. Hai bisogno di modi chiari per separare il lavoro per percorso, condividere artifact in modo sicuro e impedire che job paralleli si pestino i piedi.
Un confronto pratico si riduce a quattro aspetti: setup e scalabilità dei runner, memorizzazione e scoping dei segreti, caching e artifact, e quanto è facile esprimere "costruire solo ciò che è cambiato" senza trasformare la pipeline in una zuppa fragile di regole.
Questo riguarda l'affidabilità e la manutenibilità quotidiana, non quale piattaforma ha più integrazioni o una UI più gradevole. Non sostituisce neanche le decisioni sugli strumenti di build (Gradle, Xcode, Docker, ecc.). Aiuta a scegliere il CI che rende più semplice mantenere una struttura pulita.
Come sono strutturati GitHub Actions e GitLab CI
La differenza più grande è come ciascuna piattaforma organizza le pipeline e il riuso, e questo comincia a contare quando backend, web e mobile condividono lo stesso repo.
GitHub Actions conserva l'automazione in file YAML sotto .github/workflows/. I workflow si attivano su eventi come push, pull request, schedule o esecuzioni manuali. GitLab CI si concentra su .gitlab-ci.yml nella root del repo, con file opzionali inclusi, e le pipeline generalmente partono su push, merge request, schedule e job manuali.
GitLab è costruito attorno agli stage. Definisci stage (build, test, deploy) e poi assegni i job agli stage che corrono in ordine. GitHub Actions è costruito attorno ai workflow che contengono job. I job girano in parallelo per default e aggiungi dipendenze quando qualcosa deve aspettare.
Per eseguire la stessa logica su molti target, i matrix build di GitHub sono un adattamento naturale (iOS vs Android, più versioni di Node). GitLab può ottenere un fan-out simile usando job paralleli e variabili, ma spesso ti ritrovi a collegare più pezzi da solo.
Anche il riuso appare diverso. In GitHub, i team tendono a fare affidamento su reusable workflows e composite actions. In GitLab, il riuso arriva spesso da include, template condivisi e anchor/extends YAML.
Approvals e ambienti protetti differiscono anche loro. GitHub usa spesso ambienti protetti con revisori obbligatori e secret specifici per l'ambiente così i deploy di produzione si fermano finché non sono approvati. GitLab combina comunemente branch/tag protetti, ambienti protetti e job manuali in modo che solo ruoli specifici possano eseguire un deploy.
Setup dei runner ed esecuzione dei job
Il setup dei runner è dove le due piattaforme iniziano a sentirsi diverse nell'uso quotidiano. Entrambe possono eseguire job su runner hosted (non gestisci la macchina) o su runner self-hosted (tu possiedi la macchina, gli aggiornamenti e la sicurezza). I runner hosted sono più semplici da iniziare; i runner self-hosted sono spesso necessari per velocità, strumenti speciali o accesso a reti private.
Una separazione pratica in molti team è runner Linux per backend e web, e macOS runner solo quando devi compilare iOS. Android può girare su Linux, ma è pesante, quindi dimensione del runner e spazio su disco contano.
Hosted vs self-hosted: cosa gestisci
I runner hosted sono adatti quando vuoi un setup prevedibile senza manutenzione. I runner self-hosted hanno senso quando hai bisogno di versioni specifiche di Java/Xcode, cache più veloci o accesso a reti interne.
Se scegli self-hosted, definisci i ruoli dei runner presto. La maggior parte dei repo va bene con un set ridotto: un runner Linux generale per backend/web, un runner Linux più potente per Android, un runner macOS per packaging e signing iOS, e un runner separato per i deploy con permessi più restrittivi.
Scegliere il runner giusto per job
Entrambi i sistemi ti permettono di indirizzare runner (labels in GitHub, tags in GitLab). Mantieni i nomi legati ai carichi di lavoro, come linux-docker, android o macos-xcode15.
L'isolamento è la causa di molte build instabili. File rimasti, cache condivise corrotte o strumenti installati "a mano" su una macchina self-hosted possono creare fallimenti casuali. Workspace puliti, versioni di tool bloccate e pulizie programmate dei runner ripagano rapidamente.
Capacità e permessi sono altri punti dolenti ricorrenti, specialmente con la disponibilità e il costo di macOS. Un buon default è: i runner di build possono compilare, i runner di deploy possono distribuire, e le credenziali di produzione vivono nel più piccolo insieme possibile di job.
Segreti e variabili d'ambiente
I segreti sono dove le pipeline CI multi-app diventano rischiose. I fondamenti sono simili (conservare i segreti nella piattaforma, iniettarli a runtime), ma lo scoping si sente diverso.
GitHub Actions tipicamente scopa i segreti a livello di repository e organizzazione, con un ulteriore livello Environment. Quel livello Environment è utile quando la produzione richiede un gate manuale e un set separato di valori rispetto allo staging.
GitLab CI usa variabili CI/CD a livello di progetto, gruppo o instance. Supporta anche variabili scoperte per ambiente e protezioni come "protected" (disponibile solo su branch/tag protetti) e "masked" (nascoste nei log). Questi controlli aiutano quando un monorepo serve più team.
La modalità di errore principale è l'esposizione accidentale: output di debug, un comando fallito che scrive variabili, o un artifact che include per sbaglio un file di configurazione. Tratta log e artifact come condivisibili per default.
Nelle pipeline backend + web + mobile, i segreti includono di solito credenziali cloud, URL di database e API key di terze parti, materiale di firma (certificati/provisioning iOS, keystore Android e password), token di registry (npm, Maven, CocoaPods) e token di automazione (provider email/SMS, bot chat).
Per più ambienti (dev, staging, prod), mantieni nomi consistenti e scambia i valori tramite lo scoping dell'ambiente invece di copiare job. Questo mantiene la rotazione e il controllo degli accessi gestibili.
Alcune regole che prevengono la maggior parte degli incidenti:
- Preferisci credenziali a breve durata (come OIDC verso provider cloud quando disponibile) rispetto a chiavi a lunga durata.
- Applica il principio del minimo privilegio: identità di deploy separate per backend, web e mobile.
- Maschera i segreti ed evita di stampare variabili d'ambiente, anche in caso di errori.
- Restringi i segreti di produzione a branch/tag protetti e revisori obbligatori.
- Non conservare mai segreti negli artifact di build, neanche temporaneamente.
Un esempio semplice e ad alto impatto: i job mobile dovrebbero ricevere i segreti di firma solo su release taggate, mentre i job di deploy backend possono usare un token di deploy limitato sui merge in main. Quel cambiamento riduce da solo il raggio d'azione se un job è mal configurato.
Caching e artifact per build più veloci
La maggior parte delle pipeline lente lo sono per una ragione noiosa: scaricano e ricompilano le stesse cose ripetutamente. La cache evita lavoro ripetuto. Gli artifact risolvono un problema diverso: conservare gli output esatti di una specifica esecuzione.
Cosa cache-are dipende da cosa costruisci. I backend beneficiano di cache di dipendenze e del compilatore (per esempio la cache dei moduli Go). Le build web beneficiano della cache del package manager e degli strumenti di build. Le build mobile spesso hanno bisogno di Gradle più la cache dell'SDK Android su Linux, e cache di CocoaPods o Swift Package Manager su macOS. Fai attenzione con caching aggressivo degli output iOS (come DerivedData) a meno di non comprenderne i compromessi.
Entrambe le piattaforme seguono lo stesso schema: ripristina la cache all'inizio del job, salva la cache aggiornata alla fine. La differenza quotidiana è il controllo. GitLab rende esplicito il comportamento di cache e artifact in un unico file, includendo la scadenza. GitHub Actions spesso si affida ad action separate per il caching, che è flessibile ma più facile da configurare male.
Le chiavi di cache contano di più nei monorepo. Buone chiavi cambiano quando cambiano gli input, e restano stabili altrimenti. I lockfile (go.sum, pnpm-lock.yaml, yarn.lock e simili) dovrebbero guidare la chiave. Aiuta anche includere un hash della cartella specifica dell'app che stai costruendo invece che dell'intero repo, e mantenere cache separate per app così una modifica non invalida tutto.
Usa gli artifact per i deliverable che vuoi conservare da quella esecuzione: bundle di rilascio, APK/IPA, report di test, file di coverage e metadati di build. Le cache sono accelerazioni best-effort; gli artifact sono registri.
Se le build sono ancora lente, cerca cache sovradimensionate, chiavi che cambiano a ogni run (timestamp e SHA dei commit sono colpevoli comuni) e output cached che non sono riutilizzabili tra runner.
Adattamento al monorepo: più pipeline senza caos
Un monorepo diventa disordinato quando ogni push attiva test backend, build web e signing mobile, anche se hai cambiato solo una README. Il pattern pulito è: rileva cosa è cambiato, esegui solo i job che contano.
In GitHub Actions questo spesso significa workflow separati per app con filtri sui percorsi così ogni workflow parte solo quando cambiano file nell'area sua. In GitLab CI, spesso significa un unico file di pipeline che usa rules:changes (o child pipeline) per creare o saltare gruppi di job in base ai percorsi.
I package condivisi sono dove si rompe la fiducia. Se packages/auth cambia, sia backend che web potrebbero dover essere ricostruiti anche se le loro cartelle non sono cambiate. Tratta i percorsi condivisi come trigger per più pipeline e tieni chiare le boundary di dipendenza.
Una mappatura di trigger semplice che riduce le sorprese:
- I job backend corrono su cambiamenti in
backend/**opackages/**. - I job web corrono su cambiamenti in
web/**opackages/**. - I job mobile corrono su cambiamenti in
mobile/**opackages/**. - Le modifiche solo alla documentazione eseguono controlli veloci (formatting, spellcheck).
Parallelizza ciò che è sicuro (unit test, linting, build web). Serializza ciò che deve essere controllato (deploy, release su store). Sia needs di GitLab sia le dipendenze dei job in GitHub ti aiutano a eseguire controlli rapidi all'inizio e fermare il resto se falliscono.
Mantieni il signing mobile isolato dalla CI di tutti i giorni. Metti le chiavi di firma in un ambiente dedicato con approvazione manuale e esegui il signing solo su release taggate o su branch protetti. Le pull request normali possono comunque costruire app non firmate per la validazione senza esporre segreti sensibili.
Passo dopo passo: una pipeline pulita per backend, web e mobile
Una pipeline multi-app pulita parte da una nomenclatura che renda l'intento ovvio. Scegli un pattern e mantienilo così le persone possono leggere i log e capire cosa è stato eseguito.
Uno schema che resta leggibile:
- Pipeline:
pr-checks,main-build,release - Ambienti:
dev,staging,prod - Artifact:
backend-api,web-bundle,mobile-debug,mobile-release
Da lì, mantieni i job piccoli e promuovi solo ciò che è passato nei controlli precedenti:
-
PR checks (ogni pull request): esegui test veloci e lint solo per le app che sono cambiate. Per il backend, costruisci un artifact distribuibile (un'immagine container o un bundle server) e conservalo così i passaggi successivi non lo ricostruiscano.
-
Web build (PR + main): costruisci l'app web in un bundle statico. Nelle PR conserva l'output come artifact (o deploya in un ambiente preview se lo hai). Su main, produci un bundle versionato idoneo per
devostaging. -
Mobile debug builds (solo PR): costruisci un APK/IPA di debug. Non firmare per il release. L'obiettivo è feedback rapido e un file che i tester possano installare.
-
Release builds (solo tag): quando viene pushato un tag come
v1.4.0, esegui build complete di backend e web più build mobile firmate per il rilascio. Genera output pronti per gli store e conserva note di rilascio insieme agli artifact. -
Approvazioni manuali: posiziona approvazioni tra
stagingeprod, non prima dei test di base. Gli sviluppatori possono attivare build, ma solo i ruoli approvati dovrebbero distribuire in produzione e accedere ai segreti di produzione.
Errori comuni che fanno perdere tempo
I team spesso perdono settimane per abitudini di workflow che creano build instabili in modo silenzioso.
Una trappola è affidarsi troppo a runner condivisi. Quando molti progetti competono per la stessa pool, ottieni timeout casuali, job lenti e build mobile che falliscono solo nelle ore di picco. Se backend, web e mobile sono importanti, isola i job pesanti su runner dedicati (o almeno code separate) e mantieni limiti di risorse espliciti.
I segreti sono un altro perditempo. Le chiavi di firma mobile e i certificati sono facili da gestire male. Un errore comune è conservarli troppo ampiamente (disponibili a ogni branch e job) o perderli nei log verbosi. Mantieni il materiale di firma limitato a branch/tag protetti ed evita qualsiasi step che stampi valori segreti (anche stringhe base64).
Il caching può ritorcersi contro quando i team cache-ano directory enormi o confondono cache e artifact. Cache-are solo input stabili. Conserva come artifact gli output di cui hai bisogno più tardi.
Infine, nei monorepo, attivare ogni pipeline a ogni cambiamento consuma minuti e pazienza. Se qualcuno modifica una README e ricostruisci iOS, Android, backend e web, le persone smettono di fidarsi della CI.
Una checklist rapida che aiuta:
- Usa regole basate sui percorsi così solo le app interessate vengono eseguite.
- Separa job di test da job di deploy.
- Mantieni le chiavi di firma limitate ai workflow di release.
- Cache-are input piccoli e stabili, non intere cartelle di build.
- Pianifica capacità runner prevedibile per build mobile pesanti.
Controlli rapidi prima di scegliere una piattaforma
Prima di decidere, fai alcuni controlli che rispecchino come lavori davvero. Ti eviteranno di scegliere uno strumento che va bene per una app ma diventa doloroso quando aggiungi build mobile, più ambienti e processi di rilascio.
Concentrati su:
- Piano runner: hosted, self-hosted o mix. Le build mobile spesso spingono i team verso un mix perché iOS richiede macOS.
- Piano segreti: dove vivono i segreti, chi può leggerli e come funziona la rotazione. La produzione deve essere più stretta dello staging.
- Piano cache: cosa cache-are, dove è memorizzata e come si formano le chiavi. Se la chiave cambia ad ogni commit, pagherai il costo senza il vantaggio in velocità.
- Piano monorepo: filtri di percorso e un modo pulito per condividere passaggi comuni (lint, test) senza copy-paste.
- Piano di rilascio: tag, approvazioni e separazione degli ambienti. Sii esplicito su chi può promuovere in produzione e quale prova serve.
Metti sotto pressione quelle risposte con uno scenario piccolo. In un monorepo con backend in Go, una web app Vue e due app mobile: una modifica solo alla documentazione dovrebbe fare quasi nulla; una modifica al backend dovrebbe eseguire i test backend e costruire un artifact API; una modifica UI mobile dovrebbe costruire solo Android e iOS.
Se non riesci a descrivere quel flusso in una pagina (trigger, cache, segreti, approvazioni), fai un pilot di una settimana su entrambe le piattaforme usando lo stesso repo. Scegli quella che risulta noiosa e prevedibile.
Esempio: un flusso realistico di build e rilascio per monorepo
Immagina un repo con tre cartelle: backend/ (Go), web/ (Vue) e mobile/ (iOS e Android).
Nella giornata, vuoi feedback veloci. Nei rilasci, vuoi build complete, signing e passi di publish.
Una divisione pratica:
- Branch feature: esegui lint + unit test per le parti cambiate, costruisci backend e web e opzionalmente esegui una debug build Android. Salta iOS a meno che non sia necessario.
- Tag di rilascio: esegui tutto, crea artifact versionati, firma le app mobile e carica immagini/binari nello storage di rilascio.
La scelta dei runner cambia quando è coinvolto il mobile. Go e Vue vanno bene su Linux quasi ovunque. iOS richiede runner macOS, che possono guidare la decisione più di ogni altra cosa. Se il team vuole pieno controllo delle macchine di build, GitLab CI con runner self-hosted può essere più facile da gestire in flotta. Se preferisci meno lavoro operativo e setup rapido, i runner hosted di GitHub sono comodi, ma i minuti macOS e la loro disponibilità diventano parte della tua pianificazione.
Il caching è dove si risparmia davvero tempo, ma la cache migliore dipende dall'app. Per Go, cache-are download dei moduli e la cache di build. Per Vue, cache-are il package manager store e ricostruire solo quando cambiano i lockfile. Per mobile, cache-are Gradle e l'SDK Android su Linux; cache-are CocoaPods o Swift Package Manager su macOS, e aspettati cache più grandi e più invalidazioni.
Una regola di decisione che regge: se il codice è già ospitato su una piattaforma, comincia da lì. Cambia solo se i runner (soprattutto macOS), i permessi o la compliance ti costringono a farlo.
Prossimi passi: scegliere, standardizzare e automatizzare in sicurezza
Scegli lo strumento che corrisponde a dove sono già il codice e le persone. La differenza si manifesta spesso nell'attrito quotidiano: revisioni, permessi e quanto è veloce diagnosticare una build rotta.
Inizia semplice: una pipeline per app (backend, web, mobile). Una volta stabile, estrai i passaggi condivisi in template riusabili così riduci il copy-paste senza cancellare la responsabilità.
Scrivi lo scoping dei segreti come scriveresti chi ha le chiavi di un ufficio. I segreti di produzione non dovrebbero essere leggibili da ogni branch. Imposta un promemoria per la rotazione (ogni trimestre è meglio di mai) e accorda una procedura di revoca d'emergenza.
Se costruisci con un generatore no-code che produce codice sorgente reale, tratta generazione/export come uno step CI di prima classe. Per esempio, AppMaster (appmaster.io) genera backend in Go, web in Vue3 e app mobile in Kotlin/SwiftUI, così la pipeline può rigenerare codice su modifica e poi costruire solo i target interessati.
Una volta che hai un flusso di cui il team si fida, rendilo lo standard per i nuovi repo e mantienilo noioso: trigger chiari, runner prevedibili, segreti stretti e rilasci che vengono eseguiti solo quando lo vuoi davvero.
FAQ
Di solito conviene rimanere sulla piattaforma dove il codice e il team sono già attivi; cambia solo se i runner (soprattutto macOS), i permessi o i requisiti di conformità ti costringono a farlo. Il costo quotidiano è spesso legato alla disponibilità dei runner, al controllo dei segreti e a quanto è facile esprimere “costruire solo ciò che è cambiato” senza regole fragili.
GitHub Actions tende a sembrare più semplice per un avvio rapido e per i matrix build, con workflow distribuiti su più file YAML. GitLab CI spesso risulta più centralizzato e guidato da stage, il che può facilitare la comprensione quando la pipeline cresce e vuoi un unico posto per controllare cache, artifact e l'ordine dei job.
Considera il macOS una risorsa scarsa e usalo solo quando serve realmente per packaging o signing iOS. Una baseline comune è: runner Linux per backend e web, un runner Linux più potente per Android, e un runner macOS riservato ai job iOS, con un runner separato per i deploy che abbia permessi più restrittivi.
Il "runner drift" avviene quando lo stesso job si comporta diversamente perché SDK e tool variano tra le macchine. Risolvilo bloccando le versioni degli strumenti, evitando installazioni manuali sui runner self-hosted, usando workspace puliti e pulendo o ricostruendo periodicamente le immagini dei runner per non accumulare differenze invisibili nel tempo.
Rendi i segreti disponibili solo al più ristretto insieme di job che ne hanno bisogno e mantieni i segreti di produzione dietro branch/tag protetti e approvazioni. Per il mobile, la scelta più sicura è iniettare il materiale di signing solo per i release taggati, mentre le pull request possono produrre build di debug non firmate per la validazione.
Usa le cache per velocizzare lavori ripetuti e gli artifact per conservare gli output esatti di una singola esecuzione. La cache è un aiuto best-effort che può cambiare nel tempo; un artifact è un deliverable conservato, come un bundle di rilascio, un report di test o un APK/IPA di cui vuoi tracciare la provenienza.
Basare le chiavi di cache su input stabili come i lockfile e limitarle alla parte del repo che stai costruendo in modo che cambi non correlati non invalidino tutto. Evita chiavi che cambiano a ogni esecuzione (timestamp o SHA completo) e mantieni cache separate per ogni app così backend, web e mobile non si sovrascrivono a vicenda.
Configura trigger basati sui percorsi in modo che cartelle di documentazione o altre cartelle non avviino job costosi, e tratta le cartelle condivise come trigger espliciti per le app che ne dipendono. Se una cartella condivisa cambia, è normale ricostruire più target, ma rendi esplicita quella mappatura così la pipeline resta prevedibile.
Metti le chiavi di signing e le credenziali store fuori dalle normali esecuzioni CI, proteggendole dietro tag, branch protetti e approvazioni. Per le pull request, costruisci varianti di debug senza signing di rilascio così ottieni feedback veloce senza esporre credenziali ad alto rischio.
Sì, ma tratta la generazione come uno step di prima classe con input e output chiari in modo che sia facile fare caching e rieseguire in modo prevedibile. Se usi uno strumento come AppMaster (appmaster.io) che genera codice reale, rigenera sulle modifiche rilevanti e poi costruisci solo i target interessati (backend, web o mobile) in base a quello che effettivamente cambia dopo la generazione.


