Automatic Deployment con Github Webhooks 
Tags: tutorial, javascript, devops, productivity
Published: 2022-02-22 19:08:14

## Introducción 
Llamo Automatic Deployment al proceso que consiste en agilizar y automatizar el deploy de un sitio en cualquier plataforma. En el ejemplo que voy a presentar lo haré de forma local, pero se puede replicar en una máquina virtual.

## Motivación
Cuando tenía que hacer el deploy de mi portfolio en Digital Ocean (o en su momento AWS) tenía que hacer siempre los mismos pasos repetitivos: 

1. Hacer push de los cambios al repositorio
2. Ingresar por SSH a la máquina virtual
3. Dirigirme a la ruta del proyecto
4. Hacer pull de los últimos cambios
5. Instalar las dependencias
6. Hacer build
7. Reiniciar el administrador de procesos (PM2) para que se apliquen los últimos cambios

Eran muchos pasos, y siempre tenía que hacer los mismos. Esto me motivó a buscar una solución para ahorrarme pasos.

## Solución

Con la ayuda de Github Webhooks, y un pequeño app hecha en node, pude reducir estos siete pasos a uno solo.

La solución que encontré, consistia en un server ubicado en la máquina virtual que se encargaba de 'escuchar' los eventos push que se hacían en la rama master de mi repositorio del Portfolio. Entonces cuando escuchaba que se le había hecho un push, ejecutaba un script para correr el build de mi portfolio, y realizar el deploy.

## Paso a paso

### Server para Webhook

[Repositorio](https://github.com/gonza-lab/webhook-server). Este servidor se encargará de manejar los payloads entrantes y ejecutar el script de deploy. Lo haremos en Node con Express.

Primero hacemos el set-up de npm en nuestro proyecto con `npm init`, luego instalamos express con `npm install express`.

Armamos un servidor básico: 
```js
const express = require('express');
const { exec } = require('child_process');

const app = express();
app.use(express.json());

const PORT =  5000;
const PRODUCTION_BRANCH = 'master';

app.post('/', (req, res) => {
	console.log('Recibo una request de Github...')
	const branch = req.body.ref.split('/')[2]
	
	console.log('Se hizo push en la rama', branch)
	if(branch === PRODUCTION_BRANCH) {
		exec('echo Realizo deploy...', (error, stdout, stderr) => {
			if (error) {
  		  console.error(`exec error: ${error}`);
  		  return;
  		}
  		console.log(stdout);
  		console.error(stderr);
		});
	}
	
	res.sendStatus(200);
})

app.listen(PORT, () => {
	console.log(`Webhook server running on port ${PORT}`);
})
```

Vayamos por partes explicando el código.

Creamos una aplicación express e indicamos que vamos a utilizar un middleware para recibir JSON. Ese será el tipo en el que se enviará el payload. 
```js
const app = express();
app.use(express.json());
```

En PORT ponemos el puerto en el que correra el servior, y en BRANCH_EVENT la rama de producción. En este caso master. 
```js
const PORT =  5000;
const PRODUCTION_BRANCH = 'master';
```

Con `app.post('/',` le decimos a nuestra app de express que vamos a escuchar los `POST` en la ruta `/`. Luego en `branch` guardamos la rama en la que se está realizando el push. Esta información la voy a obtener del payload que nos envía Github. [Acá podemos ver un ejemplo de un webhook payload](https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#webhook-payload-example-35). Luego preguntamos si la rama a la que se le hizo push es la rama de producción, en caso afirmativo ejecutamos el script que querramos. Estamos utilizando la función `exec` de [`child_process`](https://nodejs.org/api/child_process.html#child_processexeccommand-options-callback) para ejecutar scripts.
```js
app.post('/', (req, res) => {
	console.log('Recibo una request de Github...')
	const branch = req.body.ref.split('/')[2]
	
	console.log('Se hizo push en la rama', branch)
	if(branch === PRODUCTION_BRANCH) {
		exec('echo Realizo deploy...', (error, stdout, stderr) => {
			if (error) {
  		  console.error(`exec error: ${error}`);
  		  return;
  		}
  		console.log(stdout);
  		console.error(stderr);
		});
	}
	
	res.sendStatus(200);
})
```

Este server estará en la máquina virtual junto al proyecto que querramos hacer deploy por medio de webhook. En mi caso lo voy a probar en local, para eso voy a levantar el server y exponerlo a internet. Esto lo podemos hacer con [Localtunnel](https://theboroer.github.io/localtunnel-www/). Lo instalamos con npm y ejecutamos `lt --port {PORT}`. En PORT ponemos el puerto en el que correrá el servidor en nuestro local. En mi caso 5000. Una vez hecho esto, Localtunnel nos devolverá un link por consola que será al que le deberá pegar Github cuando se accione el evento que configuremos.

Una vez que levantamos Localtunnel, levantamos nuestro servidor.

![URL que nos devuelve LocalTunnel](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ifoae6jgal2603p3fhoh.png)

Ya tenemos todo listo para configurar Github Webhook.

### Github Webhooks

En el repositorio del proyecto que queremos que emita el evento, hay que dirigirse a la pestaña _settings_ y _webhooks_. Dentro de ese lugar presionaremos _Add webhook_. Veremos los siguientes campos: 

- Payload URL: Aquí tendremos que ingresar la URL a la que se le enviará el payload. Aquí tendrás que ingresar la URL que te regresó Localtunnel, en mi caso: https://warm-firefox-91.loca.lt.
- Content type: Este será el tipo de contenido que tendrá el payload. Seleccionaremos application/json.
- Secret: Sirve para reconocer la request que enviará Github. Cuando se establece su token secreto, GitHub lo usa para crear una firma hasheada con cada payload. Para este ejemplo lo dejaremos vacío, pero en ambientes productivos recomiendo completerlo.

Quedaría así: 

![Formulario de creación de Webhook en Github](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eiatphyrq4rfpt870b1y.png)

Lo guardamos.

### Demo

Una vez que tenemos configurado los webhooks de nuestro repositorio, y el webhook server expuesto a internet, podremos probar si funciona o no haciendo un push en cualquier rama. 
- Si hacemos push en la rama de producción que indicamos en el código, debería salir un mensaje como 'Realizo deploy...'
- Si hacemos push en otra rama que no sea la de producción, no saldrá el mensaje de 'Realizo deploy...'

![Terminal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9vi5bha2xmcbwsqsa5vb.gif)

## Ahora es tu turno

Te invito a descargarte el repo e implementar esta característica para aprender y optimizar tu tiempo :D

![Your turn](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oxlc25gsfy1s1wrt5lug.gif)

Cualquier duda o consulta me podés escribir por [Linkedin](https://www.linkedin.com/in/gonzadev/) o dejar un comentario en este post.

Gracias por leer!