Distribuisci automaticamente applicazioni Web utilizzando GitHub Webhook

Pubblicato: 2022-03-11

Chiunque sviluppi applicazioni Web e tenti di eseguirle sui propri server non gestiti è consapevole del noioso processo coinvolto nella distribuzione dell'applicazione e nel push degli aggiornamenti futuri. I provider della piattaforma come servizio (PaaS) hanno semplificato la distribuzione di applicazioni Web senza dover eseguire il processo di provisioning e configurazione dei singoli server, in cambio di un leggero aumento dei costi e di una diminuzione della flessibilità. PaaS potrebbe aver semplificato le cose, ma a volte abbiamo ancora bisogno o vogliamo distribuire applicazioni sui nostri server non gestiti. All'inizio l'automazione di questo processo di distribuzione delle applicazioni Web sul tuo server può sembrare opprimente, ma in realtà trovare un semplice strumento per automatizzare questo potrebbe essere più facile di quanto pensi. Quanto sarà facile implementare questo strumento dipende molto dalla semplicità delle tue esigenze, ma di certo non è difficile da raggiungere e probabilmente può aiutarti a risparmiare un sacco di tempo e fatica eseguendo i noiosi bit ripetitivi dell'applicazione web schieramenti.

Molti sviluppatori hanno escogitato i propri metodi per automatizzare i processi di distribuzione delle proprie applicazioni web. Poiché la modalità di distribuzione delle applicazioni Web dipende molto dall'esatto stack tecnologico utilizzato, queste soluzioni di automazione variano l'una dall'altra. Ad esempio, i passaggi necessari per la distribuzione automatica di un sito Web PHP sono diversi dalla distribuzione di un'applicazione Web Node.js. Esistono altre soluzioni, come Dokku, che sono piuttosto generiche e queste cose (chiamate buildpack) funzionano bene con una gamma più ampia di stack tecnologici.

applicazioni web e webhook

In questo tutorial, daremo un'occhiata alle idee fondamentali alla base di un semplice strumento che puoi creare per automatizzare le distribuzioni delle tue applicazioni Web utilizzando webhook, buildpack e Procfile di GitHub. Il codice sorgente del programma prototipo che esploreremo in questo articolo è disponibile su GitHub.

Introduzione alle applicazioni Web

Per automatizzare la distribuzione della nostra applicazione web, scriveremo un semplice programma Go. Se non hai familiarità con Go, non esitare a seguire, poiché i costrutti di codice utilizzati in questo articolo sono abbastanza semplici e dovrebbero essere facili da capire. Se ne hai voglia, puoi probabilmente trasferire l'intero programma in una lingua a tua scelta abbastanza facilmente.

Prima di iniziare, assicurati di avere la distribuzione Go installata sul tuo sistema. Per installare Go, puoi seguire i passaggi descritti nella documentazione ufficiale.

Successivamente, puoi scaricare il codice sorgente di questo strumento clonando il repository GitHub. Questo dovrebbe semplificarti il ​​seguito poiché i frammenti di codice in questo articolo sono etichettati con i nomi di file corrispondenti. Se vuoi, puoi provarlo subito.

Uno dei principali vantaggi dell'utilizzo di Go per questo programma è che possiamo costruirlo in un modo in cui abbiamo dipendenze esterne minime. Nel nostro caso, per eseguire questo programma su un server dobbiamo solo assicurarci di avere installato Git e Bash. Poiché i programmi Go sono compilati in binari collegati staticamente, puoi compilare il programma sul tuo computer, caricarlo sul server ed eseguirlo quasi senza sforzo. Per la maggior parte delle altre lingue popolari di oggi, ciò richiederebbe un mastodontico ambiente di runtime o un interprete installato sul server solo per eseguire l'automa di distribuzione. I programmi Go, se eseguiti correttamente, possono anche essere molto facili da utilizzare su CPU e RAM, il che è qualcosa che desideri da programmi come questo.

Webhook GitHub

Con GitHub Webhook, è possibile configurare il tuo repository GitHub per emettere eventi ogni volta che qualcosa cambia all'interno del repository o qualche utente esegue azioni particolari sul repository ospitato. Ciò consente agli utenti di iscriversi a questi eventi e ricevere notifiche tramite chiamate URL dei vari eventi che si verificano intorno al tuo repository.

Creare un webhook è molto semplice:

  1. Vai alla pagina delle impostazioni del tuo repository
  2. Fai clic su "Webhook e servizi" nel menu di navigazione a sinistra
  3. Fare clic sul pulsante "Aggiungi webhook".
  4. Imposta un URL e, facoltativamente, un segreto (che consentirà al destinatario di verificare il payload)
  5. Effettuare altre scelte sul modulo, se necessario
  6. Invia il modulo facendo clic sul pulsante verde "Aggiungi webhook".

Webhook Github

GitHub fornisce un'ampia documentazione sui Webhook e su come funzionano esattamente, quali informazioni vengono fornite nel payload in risposta a vari eventi, ecc. Ai fini di questo articolo, siamo particolarmente interessati all'evento "push" che viene emesso ogni volta che qualcuno esegue il push a qualsiasi ramo del repository.

Pacchetti di costruzione

I Buildpack sono praticamente standard al giorno d'oggi. Utilizzati da molti provider PaaS, i buildpack consentono di specificare come verrà configurato lo stack prima della distribuzione di un'applicazione. Scrivere buildpack per la tua applicazione web è davvero facile, ma il più delle volte una rapida ricerca sul web può trovarti un buildpack che puoi usare per la tua applicazione web senza alcuna modifica.

Se hai distribuito l'applicazione su PaaS come Heroku, potresti già sapere cosa sono i buildpack e dove trovarli. Heroku ha una documentazione completa sulla struttura dei pacchetti di build e un elenco di alcuni pacchetti di build popolari ben costruiti.

Il nostro programma di automazione utilizzerà lo script di compilazione per preparare l'applicazione prima di avviarla. Ad esempio, una build Node.js di Heroku analizza il file package.json, scarica una versione appropriata di Node.js e scarica le dipendenze NPM per l'applicazione. Vale la pena notare che per semplificare le cose, non avremo un ampio supporto per i buildpack nel nostro programma di prototipi. Per ora, assumeremo che gli script buildpack siano scritti per essere eseguiti con Bash e che verranno eseguiti su una nuova installazione di Ubuntu così com'è. Se necessario, puoi facilmente estenderlo in futuro per soddisfare esigenze più esoteriche.

Profili

I profili sono semplici file di testo che ti consentono di definire i vari tipi di processi che hai nella tua applicazione. Per la maggior parte delle applicazioni semplici, idealmente avresti un unico processo "web" che sarebbe il processo che gestisce le richieste HTTP.

Scrivere profili è facile. Definisci un tipo di processo per riga digitandone il nome, seguito da due punti, seguito dal comando che genererà il processo:

 <type>: <command>

Ad esempio, se stavi lavorando con un'applicazione web basata su Node.js, per avviare il server web devi eseguire il comando "node index.js". Puoi semplicemente creare un Procfile nella directory di base del codice e chiamarlo "Procfile" con quanto segue:

 web: node index.js

Richiederemo alle applicazioni di definire i tipi di processo in Procfiles in modo da poterli avviare automaticamente dopo aver inserito il codice.

Gestione degli eventi

All'interno del nostro programma, dobbiamo includere un server HTTP che ci consentirà di ricevere richieste POST in arrivo da GitHub. Dovremo dedicare un percorso URL per gestire queste richieste da GitHub. La funzione che gestirà questi payload in arrivo sarà simile a questa:

 // hook.go type HookOptions struct { App *App Secret string } func NewHookHandler(o *HookOptions) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { evName := r.Header.Get("X-Github-Event") if evName != "push" { log.Printf("Ignoring '%s' event", evName) return } body, err := ioutil.ReadAll(r.Body) if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } if o.Secret != "" { ok := false for _, sig := range strings.Fields(r.Header.Get("X-Hub-Signature")) { if !strings.HasPrefix(sig, "sha1=") { continue } sig = strings.TrimPrefix(sig, "sha1=") mac := hmac.New(sha1.New, []byte(o.Secret)) mac.Write(body) if sig == hex.EncodeToString(mac.Sum(nil)) { ok = true break } } if !ok { log.Printf("Ignoring '%s' event with incorrect signature", evName) return } } ev := github.PushEvent{} err = json.Unmarshal(body, &ev) if err != nil { log.Printf("Ignoring '%s' event with invalid payload", evName) http.Error(w, "Bad Request", http.StatusBadRequest) return } if ev.Repo.FullName == nil || *ev.Repo.FullName != o.App.Repo { log.Printf("Ignoring '%s' event with incorrect repository name", evName) http.Error(w, "Bad Request", http.StatusBadRequest) return } log.Printf("Handling '%s' event for %s", evName, o.App.Repo) err = o.App.Update() if err != nil { return } }) }

Iniziamo verificando il tipo di evento che ha generato questo payload. Poiché siamo interessati solo all'evento "push", possiamo ignorare tutti gli altri eventi. Anche se configuri il webhook per emettere solo eventi "push", ci sarà comunque almeno un altro tipo di evento che puoi aspettarti di ricevere sull'endpoint del tuo hook: "ping". Lo scopo di questo evento è determinare se il webhook è stato configurato correttamente su GitHub.

Successivamente, leggiamo l'intero corpo della richiesta in arrivo, ne calcoliamo HMAC-SHA1 utilizzando lo stesso segreto che utilizzeremo per configurare il nostro webhook e determiniamo la validità del payload in arrivo confrontandolo con la firma inclusa nell'intestazione del richiesta. Nel nostro programma, ignoriamo questo passaggio di convalida se il segreto non è configurato. In una nota a margine, potrebbe non essere un'idea saggia leggere l'intero corpo senza almeno avere una sorta di limite superiore sulla quantità di dati con cui vorremo trattare qui, ma manteniamo le cose semplici per concentrarci sugli aspetti critici di questo strumento.

Quindi utilizziamo una struttura dalla libreria client GitHub per Go per annullare il marshalling del payload in entrata. Poiché sappiamo che si tratta di un evento "push", possiamo utilizzare la struttura PushEvent. Usiamo quindi la libreria di codifica json standard per annullare il marshalling del payload in un'istanza dello struct. Eseguiamo un paio di controlli di integrità e, se tutto è a posto, invochiamo la funzione che avvia l'aggiornamento della nostra applicazione.

Aggiornamento dell'applicazione

Dopo aver ricevuto una notifica di evento sul nostro endpoint webhook, possiamo iniziare ad aggiornare la nostra applicazione. In questo articolo daremo un'occhiata a un'implementazione abbastanza semplice di questo meccanismo e ci sarà sicuramente spazio per miglioramenti. Tuttavia, dovrebbe essere qualcosa che ci consentirà di iniziare con un processo di distribuzione automatizzato di base.

diagramma di flusso dell'applicazione webhook

Inizializzazione del repository locale

Questo processo inizierà con un semplice controllo per determinare se è la prima volta che proviamo a distribuire l'applicazione. Lo faremo controllando se la directory del repository locale esiste. Se non esiste, inizializzeremo prima il nostro repository locale:

 // app.go func (a *App) initRepo() error { log.Print("Initializing repository") err := os.MkdirAll(a.repoDir, 0755) // Check err cmd := exec.Command("git", "--git-dir="+a.repoDir, "init") cmd.Stderr = os.Stderr err = cmd.Run() // Check err cmd = exec.Command("git", "--git-dir="+a.repoDir, "remote", "add", "origin", fmt.Sprintf("[email protected]:%s.git", a.Repo)) cmd.Stderr = os.Stderr err = cmd.Run() // Check err return nil }

Questo metodo sulla struttura dell'app può essere utilizzato per inizializzare il repository locale e i suoi meccanismi sono estremamente semplici:

  1. Crea una directory per il repository locale se non esiste.
  2. Usa il comando "git init" per creare un repository nudo.
  3. Aggiungi un URL per il repository remoto al nostro repository locale e chiamalo "origine".

Una volta che abbiamo un repository inizializzato, il recupero delle modifiche dovrebbe essere semplice.

Recupero delle modifiche

Per recuperare le modifiche dal repository remoto, dobbiamo solo invocare un comando:

 // app.go func (a *App) fetchChanges() error { log.Print("Fetching changes") cmd := exec.Command("git", "--git-dir="+a.repoDir, "fetch", "-f", "origin", "master:master") cmd.Stderr = os.Stderr return cmd.Run() }

Eseguendo un "git fetch" per il nostro repository locale in questo modo, possiamo evitare problemi con Git che non è in grado di avanzare rapidamente in determinati scenari. Non che i recuperi forzati siano qualcosa su cui dovresti fare affidamento, ma se hai bisogno di eseguire un push forzato sul tuo repository remoto, questo lo gestirà con grazia.

Applicazione di compilazione

Dato che stiamo usando gli script dei buildpack per compilare le nostre applicazioni che vengono distribuite, il nostro compito qui è relativamente semplice:

 // app.go func (a *App) compileApp() error { log.Print("Compiling application") _, err := os.Stat(a.appDir) if !os.IsNotExist(err) { err = os.RemoveAll(a.appDir) // Check err } err = os.MkdirAll(a.appDir, 0755) // Check err cmd := exec.Command("git", "--git-dir="+a.repoDir, "--work-tree="+a.appDir, "checkout", "-f", "master") cmd.Dir = a.appDir cmd.Stderr = os.Stderr err = cmd.Run() // Check err buildpackDir, err := filepath.Abs("buildpack") // Check err cmd = exec.Command("bash", filepath.Join(buildpackDir, "bin", "detect"), a.appDir) cmd.Dir = buildpackDir cmd.Stderr = os.Stderr err = cmd.Run() // Check err cacheDir, err := filepath.Abs("cache") // Check err err = os.MkdirAll(cacheDir, 0755) // Check err cmd = exec.Command("bash", filepath.Join(buildpackDir, "bin", "compile"), a.appDir, cacheDir) cmd.Dir = a.appDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() }

Iniziamo rimuovendo la nostra precedente directory dell'applicazione (se presente). Successivamente, ne creiamo uno nuovo e controlliamo il contenuto del ramo principale su di esso. Usiamo quindi lo script "rileva" dal buildpack configurato per determinare se l'applicazione è qualcosa che possiamo gestire. Quindi, creiamo una directory "cache" per il processo di compilazione del pacchetto di build, se necessario. Poiché questa directory persiste tra le build, può succedere che non sia necessario creare una nuova directory perché ne esisterà già una da un processo di compilazione precedente. A questo punto, possiamo invocare lo script "compila" dal buildpack e farlo preparare tutto il necessario per l'applicazione prima del lancio. Quando i buildpack vengono eseguiti correttamente, possono gestire autonomamente la memorizzazione nella cache e il riutilizzo delle risorse precedentemente memorizzate nella cache.

Riavvio dell'applicazione

Nella nostra implementazione di questo processo di distribuzione automatizzato, arresteremo i vecchi processi prima di avviare il processo di compilazione e quindi avvieremo i nuovi processi una volta completata la fase di compilazione. Sebbene ciò semplifichi l'implementazione dello strumento, lascia alcuni modi potenzialmente sorprendenti per migliorare il processo di distribuzione automatizzata. Per migliorare questo prototipo, probabilmente puoi iniziare assicurandoti zero tempi di inattività durante gli aggiornamenti. Per ora, continueremo con l'approccio più semplice:

 // app.go func (a *App) stopProcs() error { log.Print(".. stopping processes") for _, n := range a.nodes { err := n.Stop() if err != nil { return err } } return nil } func (a *App) startProcs() error { log.Print("Starting processes") err := a.readProcfile() if err != nil { return err } for _, n := range a.nodes { err = n.Start() if err != nil { return err } } return nil }

Nel nostro prototipo, fermiamo e avviamo i vari processi iterando su un array di nodi, dove ogni nodo è un processo corrispondente a una delle istanze dell'applicazione (come configurato prima di avviare questo strumento sul server). All'interno del nostro strumento, teniamo traccia dello stato corrente del processo per ciascun nodo. Conserviamo anche file di registro individuali per loro. Prima che tutti i nodi vengano avviati, a ciascuno viene assegnata una porta univoca a partire da un determinato numero di porta:

 // node.go func NewNode(app *App, name string, no int, port int) (*Node, error) { logFile, err := os.OpenFile(filepath.Join(app.logsDir, fmt.Sprintf("%s.%d.txt", name, no)), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { return nil, err } n := &Node{ App: app, Name: name, No: no, Port: port, stateCh: make(chan NextState), logFile: logFile, } go func() { for { next := <-n.stateCh if n.State == next.State { if next.doneCh != nil { close(next.doneCh) } continue } switch next.State { case StateUp: log.Printf("Starting process %s.%d", n.Name, n.No) cmd := exec.Command("bash", "-c", "for f in .profile.d/*; do source $f; done; "+n.Cmd) cmd.Env = append(cmd.Env, fmt.Sprintf("HOME=%s", n.App.appDir)) cmd.Env = append(cmd.Env, fmt.Sprintf("PORT=%d", n.Port)) cmd.Env = append(cmd.Env, n.App.Env...) cmd.Dir = n.App.appDir cmd.Stdout = n.logFile cmd.Stderr = n.logFile err := cmd.Start() if err != nil { log.Printf("Process %s.%d exited", n.Name, n.No) n.State = StateUp } else { n.Process = cmd.Process n.State = StateUp } if next.doneCh != nil { close(next.doneCh) } go func() { err := cmd.Wait() if err != nil { log.Printf("Process %s.%d exited", n.Name, n.No) n.stateCh <- NextState{ State: StateDown, } } }() case StateDown: log.Printf("Stopping process %s.%d", n.Name, n.No) if n.Process != nil { n.Process.Kill() n.Process = nil } n.State = StateDown if next.doneCh != nil { close(next.doneCh) } } } }() return n, nil } func (n *Node) Start() error { n.stateCh <- NextState{ State: StateUp, } return nil } func (n *Node) Stop() error { doneCh := make(chan int) n.stateCh <- NextState{ State: StateDown, doneCh: doneCh, } <-doneCh return nil }

A prima vista, questo può sembrare un po' più complicato di quello che abbiamo fatto finora. Per rendere le cose facili da capire, scomponiamo il codice sopra in quattro parti. I primi due sono all'interno della funzione "NewNode". Quando viene chiamato, popola un'istanza della struttura "Node" e genera una routine Go che aiuta ad avviare e arrestare il processo corrispondente a questo nodo. Gli altri due sono i due metodi sulla struttura "Nodo": "Start" e "Stop". Un processo viene avviato o interrotto passando un "messaggio" attraverso un particolare canale su cui questa routine Go per nodo sta tenendo d'occhio. Puoi passare un messaggio per avviare il processo o un messaggio diverso per interromperlo. Poiché i passaggi effettivi coinvolti nell'avvio o nell'arresto di un processo si verificano in un'unica routine Go, non c'è possibilità di ottenere condizioni di gara.

La routine Go avvia un ciclo infinito in cui attende un "messaggio" attraverso il canale "stateCh". Se il messaggio passato a questo canale richiede al nodo di avviare il processo (all'interno di "case StateUp"), utilizza Bash per eseguire il comando. Mentre lo fa, configura il comando per utilizzare le variabili di ambiente definite dall'utente. Reindirizza inoltre l'output standard e i flussi di errore a un file di registro predefinito.

D'altra parte, per fermare un processo (all'interno di "case StateDown"), lo uccide semplicemente. È qui che potresti probabilmente diventare creativo e invece di interrompere il processo invialo immediatamente un SIGTERM e attendi alcuni secondi prima di ucciderlo effettivamente, dando al processo la possibilità di fermarsi con grazia.

I metodi "Start" e "Stop" facilitano il passaggio del messaggio appropriato al canale. A differenza del metodo "Start", il metodo "Stop" attende effettivamente che i processi vengano terminati prima di tornare. "Start" passa semplicemente un messaggio al canale per avviare il processo e ritorna.

Combinando tutto

Infine, tutto ciò che dobbiamo fare è collegare tutto all'interno della funzione principale del programma. Qui è dove caricheremo e analizzeremo il file di configurazione, aggiorneremo il buildpack, tenteremo di aggiornare la nostra applicazione una volta e avvieremo il server Web per ascoltare i payload di eventi "push" in arrivo da GitHub:

 // main.go func main() { cfg, err := toml.LoadFile("config.tml") catch(err) url, ok := cfg.Get("buildpack.url").(string) if !ok { log.Fatal("buildpack.url not defined") } err = UpdateBuildpack(url) catch(err) // Read configuration options into variables repo (string), env ([]string) and procs (map[string]int) // ... app, err := NewApp(repo, env, procs) catch(err) err = app.Update() catch(err) secret, _ := cfg.Get("hook.secret").(string) http.Handle("/hook", NewHookHandler(&HookOptions{ App: app, Secret: secret, })) addr, ok := cfg.Get("core.addr").(string) if !ok { log.Fatal("core.addr not defined") } err = http.ListenAndServe(addr, nil) catch(err) }

Poiché richiediamo che i buildpack siano semplici repository Git, "UpdateBuildpack" (implementato in buildpack.go) esegue semplicemente un "git clone" e un "git pull" se necessario con l'URL del repository per aggiornare la copia locale del buildpack.

Provarlo

Nel caso in cui non hai ancora clonato il repository, puoi farlo ora. Se hai installato la distribuzione Go, dovrebbe essere possibile compilare subito il programma.

 mkdir hopper cd hopper export GOPATH=`pwd` go get github.com/hjr265/toptal-hopper go install github.com/hjr265/toptal-hopper

Questa sequenza di comandi creerà una directory denominata hopper, la imposterà come GOPATH, preleverà il codice da GitHub insieme alle librerie Go necessarie e compilerà il programma in un binario che puoi trovare nella directory "$GOPATH/bin". Prima di poterlo utilizzare su un server, dobbiamo creare una semplice applicazione Web con cui testarlo. Per comodità, ho creato una semplice applicazione Web Node.js simile a "Hello, world" e l'ho caricata su un altro repository GitHub che puoi fork e riutilizzare per questo test. Successivamente, dobbiamo caricare il binario compilato su un server e creare un file di configurazione nella stessa directory:

 # config.tml [core] addr = ":26590" [buildpack] url = "https://github.com/heroku/heroku-buildpack-nodejs.git" [app] repo = "hjr265/hopper-hello.js" [app.env] GREETING = "Hello" [app.procs] web = 1 [hook] secret = ""

La prima opzione nel nostro file di configurazione, "core.addr" è ciò che ci consente di configurare la porta HTTP del server Web interno del nostro programma. Nell'esempio sopra, lo abbiamo impostato su ":26590", che farà in modo che il programma ascolti i payload di eventi "push" su "http://{host}:26590/hook". Quando configuri il webhook GitHub, sostituisci semplicemente "{host}" con il nome di dominio o l'indirizzo IP che punta al tuo server. Assicurati che la porta sia aperta nel caso in cui utilizzi una sorta di firewall.

Successivamente, scegliamo un buildpack impostando il suo URL Git. Qui stiamo usando il buildpack Node.js di Heroku.

In "app", impostiamo "repo" sul nome completo del repository GitHub che ospita il codice dell'applicazione. Poiché sto ospitando l'applicazione di esempio su "https://github.com/hjr265/hopper-hello.js", il nome completo del repository è "hjr265/hopper-hello.js".

Quindi impostiamo alcune variabili di ambiente per l'applicazione e il numero di ciascun tipo di processo di cui abbiamo bisogno. E infine, scegliamo un segreto, in modo da poter verificare i payload di eventi "push" in arrivo.

Ora possiamo avviare il nostro programma di automazione sul server. Se tutto è configurato correttamente (inclusa la distribuzione delle chiavi SSH, in modo che il repository sia accessibile dal server), il programma dovrebbe recuperare il codice, preparare l'ambiente utilizzando il buildpack e avviare l'applicazione. Ora tutto ciò che dobbiamo fare è configurare un webhook nel repository GitHub per emettere eventi push e puntarlo a "http://{host}:26590/hook". Assicurati di sostituire "{host}" con il nome di dominio o l'indirizzo IP che punta al tuo server.

Per testarlo finalmente, apporta alcune modifiche all'applicazione di esempio e inviale a GitHub. Noterai che lo strumento di automazione entrerà immediatamente in azione e aggiornerà il repository sul server, compilerà l'applicazione e la riavvierà.

Conclusione

Dalla maggior parte delle nostre esperienze, possiamo dire che questo è qualcosa di abbastanza utile. L'applicazione prototipo che abbiamo preparato in questo articolo potrebbe non essere qualcosa che vorrai utilizzare su un sistema di produzione così com'è. C'è un sacco di margini di miglioramento. Uno strumento come questo dovrebbe avere una migliore gestione degli errori, supportare arresti/riavvii regolari e potresti voler usare qualcosa come Docker per contenere i processi invece di eseguirli direttamente. Potrebbe essere più saggio capire di cosa hai bisogno esattamente per il tuo caso specifico e inventare un programma di automazione per quello. O magari utilizzare un'altra soluzione molto più stabile e collaudata disponibile su Internet. Ma nel caso in cui desideri implementare qualcosa di molto personalizzato, spero che questo articolo ti aiuti a farlo e mostri quanto tempo e fatica potresti risparmiare a lungo termine automatizzando il processo di distribuzione delle applicazioni web.

Correlati: Spiegazione del flusso Git avanzato