Contenido

    Cómo crear un README dinámico para tu perfil de Github con Node.js y Github Actions

    Cómo crear un README dinámico para tu perfil de Github con Node.js y Github Actions

    17/1/2023 • 30/60 minutos de lectura

    No sé si ya estabas al tanto de los ✨ repositorios especiales ✨ que podes crear en tu cuenta de Github. Básicamente es uno que tiene tu mismo nombre de usuario. Github lo vuelve especial, ya que si dentro tiene un README.md (no tenerlo es delito igual 😳) a este lo va a mostrar en tu perfil.

    Si tenés ganas de enriquecer tu perfil, darle estilo y hacer que su contenido sea dinámico, este sencillo tutorial te va a ayudar a construirlo.

    Plantilla

    Lo primero que vamos a necesitar es establecer cuál va a ser nuestro diseño y qué información nos interesa compartir. Preparalo a mano según más te guste! Vamos a meter un poco de código para que luego, algunas partes sean dinámicas.

    Para este tutorial, yo decidí usar este diseño que cree recolectando ideas de otros usuarios:

    Diseño a usar

    Si no sabés cómo crearlo, te dejo este repositorio y web que te van a servir de inspiración para poder maquetarlo:

    También podes utilizar el mismo y hacerle tus modificaciones 😄

    Este diseño viene genial para el tutorial. Tiene dos tablas, una de últimos posts y otra de últimos videos. Si tuviésemos que actualizar manualmente esa información por cada vez que tenemos un nuevo post o un nuevo video, sería super tedioso. En su lugar, vamos a usar Github Actions para que se encargue de traernos lo último a nuestro README.md cada cierta cantidad de tiempo.
    Esta va a ser la primera de dos partes. Sólo vamos a completar la tabla de videos, en la siguiente vemos cómo scrappear un blog y cargar las últimas publicaciones!

    Requerimientos

    En este tutorial vamos a necesitar:

    • Una cuenta en Github, por supuesto
    • Git
    • Node.js (yo estoy en la versión 16)
    • El template engine que conozcas o te guste usar (yo voy a usar EJS)

    Creando el repositorio

    Primero que nada, vamos a dirigirnos a nuestro perfil de Github a crear nuestro ✨ repo especial ✨.

    En la página de inicio de Github, al lado de su imagen de perfil pueden crear un nuevo repositorio:


    Como nombre del repositorio, vamos a poner nuestro mismo nombre de usuario, para que sea ✨ especial ✨.

    ℹ️ Solo para este tutorial, yo lo nombre dinamic-readme-tutorial

    Paso importante para poder clonarlo: vamos a bajar en esta misma página y marcar la opción "Add README file"

    Una vez creado, vamos a clonarlo en nuestra máquina. Copiamos el link y nos dirigimos a nuestra terminal:


    Si utilizas SSH, copias la dirección que aparece en la imagen. Sino, el link HTTPS va a estar bien (vas a tener que iniciar sesión al clonar el repositorio)

    Clonando el repositorio

    Una vez copiado el link, vamos a nuestra terminal y ejecutamos el siguiente comando:

    $ git clone <tu-link>
    
    # Por ejemplo: git clone git@github.com:juanespinola05/dinamic-readme-tutorial.git

    Si utilizas HTTPS va a pedirte que inicies sesión.

    A continuación nos metemos en la carpeta y ya podemos comenzar:

    $ cd <nombre-de-la-carpeta>
    
    # Por ejemplo: cd dinamic-readme-tutorial

    ¡Ya podemos empezar!

    Inicializando el proyecto de Node.JS

    Para iniciar, vamos a ejecutar un comando de NPM para que nos prepare nuestro archivo package.json

    $ npm init -y

    Esto nos va a generar el siguiente archivo:

    {
      "name": "dinamic-readme-tutorial",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "repository": {
        "type": "git",
        "url": "git+https://github.com/juanespinola05/dinamic-readme-tutorial.git"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "bugs": {
        "url": "https://github.com/juanespinola05/dinamic-readme-tutorial/issues"
      },
      "homepage": "https://github.com/juanespinola05/dinamic-readme-tutorial#readme"
    }

    A partir de este punto, podes usar tu editor de código preferido para trabajar en el programa.

    Vamos a habilitar a nuestro proyecto como un módulo para poder utilizar ES Modules y top level await agregando a nuestro package.json el atributo "type"="module"

    // package.json
    {
      "name": "dinamic-readme-tutorial",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "type": "module", // Acá lo agregamos!
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "repository": {
        "type": "git",
        "url": "git+https://github.com/juanespinola05/dinamic-readme-tutorial.git"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "bugs": {
        "url": "https://github.com/juanespinola05/dinamic-readme-tutorial/issues"
      },
      "homepage": "https://github.com/juanespinola05/dinamic-readme-tutorial#readme"
    }

    Instalando las dependencias necesarias

    Lo que vamos a utilizar:

    • node-fetch para tener la fetch api (a partir de la version 17 de Node ya no es necesario ya que se encuentra implementado de forma experimental)
    • ejs para nuestro template engine
    • dotenv para importar nuestras variables de entorno
    $ npm install ejs dotenv node-fetch -e

    Vamos a crear el archivo .gitignore para evitar enviar ficheros no deseados a nuestro repositorio remoto en GitHub. Lo creamos en la raíz de nuestro y dentro escribimos:

    node_modules/

    Listos para comenzar!

    Crear plantilla

    Vamos a crear nuestra plantilla. Este archivo va a ser de extensión .ejs, la extensión que utiliza el template engine EJS. Lo vamos a utilizar como base para generar nuestro README:

    // template.ejs
    <h1>Hola, soy Juan!</h1>
    <h3>Si buscas un desarrollador de software con un enfoque creativo e innovador, ¡has encontrado a la persona adecuada!
    </h3>
    
    Con más de cinco años de experiencia en el desarrollo de aplicaciones, tengo una sólida comprensión de las mejores
    prácticas y una habilidad probada para resolver problemas complejos. Me apasiona experimentar con nuevas tecnologías y
    buscar soluciones creativas a los desafíos técnicos. Cuando no estoy programando, me gusta colaborar con otros
    desarrolladores en proyectos de código abierto y compartir mis conocimientos en mi blog y en mi cuenta de Twitter. ¡Si
    estás buscando a un desarrollador que pueda aportar un enfoque fresco y original a tu proyecto, no dudes en ponerte en
    contacto conmigo!
    
    <p align="left">
      <a href="">
        <img alt="" width="50px"
          src="https://user-images.githubusercontent.com/43545812/144034996-602b144a-16e1-41cc-99e7-c6040b20dcaf.png" />
      </a>
      <a href="">
        <img alt="" width="50px"
          src="https://user-images.githubusercontent.com/43545812/144035037-0f415fc7-9f96-4517-a370-ccc6e78a714b.png" />
      </a>
      <a href="">
        <img alt="" width="50px"
          src="https://user-images.githubusercontent.com/43545812/144035088-0dfb165f-8fe0-4d13-896c-876c29d2b128.png" />
      </a>
      <a href="">
        <img alt="" width="50px"
          src="https://user-images.githubusercontent.com/43545812/144035120-1ad5169b-91c7-4078-bef9-6a82c733f373.png" />
      </a>
    </p>
    
    <hr />
    <h3>Mis últimos posts</h3>
    <table>
      <thead>
        <tr>
          <th>Fecha</th>
          <th>Título</th>
          <th>Link</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>
            12/12/2022
          </td>
          <td>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
          </td>
          <td>
            <a href="#">Leer</a>
          </td>
        </tr>
        <tr>
          <td>
            12/12/2022
          </td>
          <td>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
          </td>
          <td>
            <a href="#">Leer</a>
          </td>
        </tr>
        <tr>
          <td>
            12/12/2022
          </td>
          <td>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
          </td>
          <td>
            <a href="#">Leer</a>
          </td>
        </tr>
        <tr>
          <td>
            12/12/2022
          </td>
          <td>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
          </td>
          <td>
            <a href="#">Leer</a>
          </td>
        </tr>
      </tbody>
    </table>
    <br />
    
    <hr />
    <h3>Mira mis últimos videos</h3>
    <table>
      <thead>
        <tr>
          <th>🎬</th>
          <th>Título</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>
            <a href="https://youtube.com">
              <img src="https://f.hellowork.com/blogdumoderateur/2022/06/youtube-astuces-vignettes.jpg" width="200" />
            </a>
          </td>
          <td>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
          </td>
        </tr>
        <tr>
          <td>
            <a href="https://youtube.com">
              <img src="https://f.hellowork.com/blogdumoderateur/2022/06/youtube-astuces-vignettes.jpg" width="200" />
            </a>
          </td>
          <td>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
          </td>
        </tr>
        <tr>
          <td>
            <a href="https://youtube.com">
              <img src="https://f.hellowork.com/blogdumoderateur/2022/06/youtube-astuces-vignettes.jpg" width="200" />
            </a>
          </td>
          <td>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
          </td>
        </tr>
      </tbody>
    </table>
    <br />

    Más adelante vamos a modificar a nuestro archivo con la sintaxis especial de EJS para que las tablas de posts y videos se generen con la infomación que vamos a buscar!

    Escribiendo el index.js

    A continuación importamos writeFileSync y readFileSync que nos van a servir para escribir y leer archivos respectivamente. También vamos a hacer uso de la función resolve del módulo path para crear nuestras rutas a nuestros archivos y ejs para hacer el renderizado

    // index.js
    import { readFileSync, writeFileSync } from 'fs'
    import { resolve } from 'path'
    import ejs from 'ejs'

    Creamos nuestras rutas:

    // index.js
    import { readFileSync, writeFileSync } from 'fs'
    import { resolve } from 'path'
    import ejs from 'ejs'
    
    // Creamos nuestras rutas a nuestros archivos
    const TEMPLATE_PATH = resolve('template.ejs')
    const README_PATH = resolve('README.md')

    Vamos a leer nuestro archivo de plantilla y guardar su información en una constante:

    // index.js
    import { readFileSync, writeFileSync } from 'fs'
    import { resolve } from 'path'
    import ejs from 'ejs'
    
    const TEMPLATE_PATH = resolve('template.ejs')
    const README_PATH = resolve('README.md')
    
    // Leemos el contenido del archivo de
    // plantilla y lo guardamos en template
    const template = readFileSync(TEMPLATE_PATH)

    Nuestro proyecto hasta el momento luce así:


    El archivo package-lock.json y la carpeta node_modules los crea Node.js.

    Buscando nuestra información

    Este es el momento de crear las funciones que van a obtener nuestra información para tenerla actualizada.
    Dependiendo de nuestro perfil y lo que queramos hacer, vamos a buscar información de distintos lugares. En mi caso, necesito traer la información desde mi blog, y tambien traer los videos de Youtube.
    La idea es simple, si tenes casos diferentes, vas a saber cómo encaminar la solución y hacer tu propia implementación para buscar tu propia información.

    Voy a crear una carpeta llamada utils/ y dentro de esta, un archivo llamado videos.js

    En el archivo videos.js procedo a escribir la función para obtener esa información.
    Para hacer fetching de datos de Youtube, podemos usar Google APIs, hacer scrapping o, en mi caso, usar una API de Rapid API que lo trae un poco más simple. Pueden ver información sobre esta en este link: Youtube v3 API Documentation
    Esta API requiere que utilice una API KEY para poder comunicarme con ella, por lo que voy a crear en la raíz de mi proyecto dos archivos: .env y .env.example. En mi archivo .env voy a colocar:

    // .env
    RAPID_API_KEY='aidh29r87h23798fyh2b1298rf'
    // .env.example
    RAPID_API_KEY=''

    El archivo .env tiene información sensible así que no debe ser enviado a nuestro repositorio remoto. Para ello, lo agregamos a nuestro archivo .gitignore

    node_modules/
    .env

    Nuestro proyecto ahora luce así:


    Ahora sí, en nuestro archivo de videos.js:


    // videos.js
    
    // Importamos la función fetch del paquete node-fetch
    import fetch from 'node-fetch'
    // Importamos config del módulo dotenv que instalamos
    import { config } from 'dotenv'
    // Lo ejecutamos para que RAPID_API_KEY esté disponible
    // en el ambiente
    config()
    
    // Creamos una constante para almacenar el ID de nuestro canal
    const CHANNEL_ID = 'UCaw6pZKpqHpK-I0spCw0eeQ'
    // Creamos la URL a la que apuntaremos
    // Los parametros que tienen indican:
    //  maxResults=3 Traer solo 3 videos
    //  part=snippet,id Traer el ID del video y sus detalles
    //  order=date ordenarlos por fecha
    const FETCH_URL =
      `https://youtube-v31.p.rapidapi.com/search?channelId=${CHANNEL_ID}&part=snippet,id&order=date&maxResults=3`
    // Creamos las opciones necesarias para nuestra petición
    const options = {
      headers: {
        'X-RapidAPI-Key': process.env.RAPID_API_KEY,
        'X-RapidAPI-Host': 'youtube-v31.p.rapidapi.com',
      },
    }
    // Creamos la función que hará el trabajo de la petición
    async function getVideosFromAPI() {
      // El try/catch nos permitirá interceptar errores si es que ocurren
      try {
        // Hacemos la petición con fetch()
        const response = await fetch(FETCH_URL, options)
        // Convertimos la respuesta a un objeto
        const data = await response.json()
    
        // La API nos responde con varios detalles sobre el video
        // Nosotros necesitamos unos pocos, asi que vamos a obtener
        // lo necesario con la función formatData que vamos a crear
        // debajo
        return formatData(data)
      } catch {
        return []
      }
    }
    
    // Esta función recibe el cuerpo de la respuesta
    // y devuelve un array de los videos con la información
    // que necesitamos
    function formatData(data) {
      // Extraemos los items (array) del cuerpo
      const { items } = data
      // Mapeamos nuestro array para obtener lo que necesitamos
      const videos = items.map((item) => ({
        id: item.id.videoId,
        thumbnail: item.snippet.thumbnails.medium.url,
        title: item.snippet.title,
        url: 'https://youtube.com/watch?v=' + item.id.videoId,
      }))
      // retornamos el nuevo array
      return videos
    }
    
    // Exportamos esta función para poder usarla desde otros ficheros
    export default getVideosFromAPI

    Una vez que creamos todo nuestra función, ya podemos importarla y llamarla desde nuestro archivo principal!

    // index.js
    ...
    // Importamos la función
    import getVideosFromAPI from './utils/videos.js'
    
    const TEMPLATE_PATH = resolve('template.ejs')
    const README_PATH = resolve('README.md')
    
    const template = readFileSync(TEMPLATE_PATH)
    
    // Creamos el objeto con la información que vamos a necesitar
    const data = {
      videos: await getVideosFromAPI() // llamamos a nuestra función
    }

    Podemos probar como venimos hasta este punto con un console.log:

    //index.js
    ...
    
    const data = {
      videos: await getVideosFromAPI() // llamamos a nuestra función
    }
    console.log(data)

    En nuestra terminal:

    $ node index.js

    Resultado:

    {
      videos:
      ;
      ;[
        {
          id: 'DWRb05qosak',
          thumbnail: 'https://i.ytimg.com/vi/DWRb05qosak/mqdefault.jpg',
          title: 'AdventJS on the fly #6: Creating Xmas decorations',
          url: 'https://youtube.com/watch?v=DWRb05qosak',
        },
        {
          id: 'gUxmFHvcXgk',
          thumbnail: 'https://i.ytimg.com/vi/gUxmFHvcXgk/mqdefault.jpg',
          title: 'Practicando CSS desde cero #4: Sección de modelos',
          url: 'https://youtube.com/watch?v=gUxmFHvcXgk',
        },
        {
          id: 'TBueCOpgvFo',
          thumbnail: 'https://i.ytimg.com/vi/TBueCOpgvFo/mqdefault.jpg',
          title: 'AdventJS on the fly #4: Box inside a box and another...',
          url: 'https://youtube.com/watch?v=TBueCOpgvFo',
        },
      ]
    }

    Modificando nuestra plantilla con EJS

    Ya que tenemos nuestra información, sólo nos queda editar nuestro archivo template para que este se encargue de, por cada video que le enviamos, crear el elemento HTML para el mismo.
    En un comienzo escribimos videos a mano en nuestro archivo template.ejs. Ahora vamos a reemplazarlos con la sintaxis de EJS para que estos se rendericen:
    Antes:

    // template.ejs
    ...
    <h3>Mira mis últimos videos</h3>
    <table>
      <thead>
        <tr>
          <th>🎬</th>
          <th>Título</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>
            <a href="https://youtube.com">
              <img src="https://f.hellowork.com/blogdumoderateur/2022/06/youtube-astuces-vignettes.jpg" width="200" />
            </a>
          </td>
          <td>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
          </td>
        </tr>
        <tr>
          <td>
            <a href="https://youtube.com">
              <img src="https://f.hellowork.com/blogdumoderateur/2022/06/youtube-astuces-vignettes.jpg" width="200" />
            </a>
          </td>
          <td>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
          </td>
        </tr>
        <tr>
          <td>
            <a href="https://youtube.com">
              <img src="https://f.hellowork.com/blogdumoderateur/2022/06/youtube-astuces-vignettes.jpg" width="200" />
            </a>
          </td>
          <td>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
          </td>
        </tr>
      </tbody>
    </table>
    ...

    Ahora:

    // template.ejs
    ...
    <h3>Mira mis últimos videos</h3>
    <table>
      <thead>
        <tr>
          <th>🎬</th>
          <th>Título</th>
        </tr>
      </thead>
      <tbody>
    <% videos.forEach(video => { %> 
      <tr>
        <td>
          <a href="<%= video.url %>">
            <img
              width="200"
              src="<%= video.thumbnail %>"
            />
          </a>
        </td>
        <td><%= video.title %></td>
      </tr>
    <% }) %> 
      </tbody>
    </table>
    ...

    Podes encontrar cómo funciona esta sintaxis en la documentación de EJS.
    El código que añadimos va a tomar videos del objeto data que creamos y va a ejecutar un forEach. Por cada video va a crear los elementos <tr> y los que tiene dentro con la información de cada uno de los videos.

    ℹ️ Nótese cómo entre la etiqueta tbodyestá todo indendato sobre el margen. Esto está hecho a posta, ya que puede pasar que al parsear el markdown, se cree un snippet de nuestro código generado en lugar de mostrarlo como queremos.

    Renderizado y guardado de la plantilla

    Y por último en nuestro index.js necesitamos crear el renderizado para guardar nuestro nuevo README.md:

    //index.js
    ...
    // Transformamos nuestra plantilla a un string
    const templateString = template.toString()
    
    // Utilizamos la función render del módulo ejs
    // y le enviamos nuestra información para que genere
    // el nuevo texto
    const renderedText = ejs.render(templateString, data)
    
    // Guardamos el texto creado en nuestro archivo
    writeFileSync(README_PATH, renderedText)

    Y listo!!!!! Para probar podemos hacer nuevamente

    $ node index.js

    Esto deberia traernos la información, formatearla, leer la plantilla, renderizarla y guardar el resultado en README.md

    Automatización con Github Actions

    Github Actions es una plataforma de CI/CD que por supuesto está integrada a github y nos va a permitir crear lo que se llaman workflows. Estos workflows son simplemente scripts donde detallamos una serie de pasos y eventos para que se ejecuten ciertas tareas en nuestros repositorios.


    Nosotros vamos a crear un workflow especial que se ejecute, por ejemplo, todos los días a las 12pm. La idea es que este proceso cargue nuestro código, lo ejecute y los nuevos cambios los guarde en nuestro repositorio. Así de esa manera, todos los días a las 12pm de forma automática Github va a actualizar nuestro README.md con la información nueva!

    ℹ️ Si te interesa conocer cómo funcionan Github Actions en detalle y aprender más sobre integración continua, podes indagar más en su documentación: Github Actions Documentation

    En nuestro proyecto vamos a crear una carpeta llamada .github/ y dentro de esta, otra llamada workflows. Dentro de esta última, vamos a crear un archivo llamado update-readme.yaml. En este archivo vamos a especificar las reglas que necesitamos, que son las siguientes:

    name: Automaticaly update README
    on:
      push:
        branches:
          - main
      schedule:
        - cron: '0 12 * * *'
    
    jobs:
      render_readme:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v2
            with:
              fetch-depth: 0
          - uses: actions/setup-node@v2
            with:
              node-version: '16'
          - name: Install dependencies
            run: npm install
          - name: Generate README.md
            run: node index.js
            env:
              RAPID_API_KEY: ${{ secrets.RAPID_API_KEY }}
          - name: Save new READ.ME
            uses: mikeal/publish-to-github-action@master
            env:
              GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
              BRANCH_NAME: 'main'

    A estos scripts se los conoce como Cron Jobs: tareas que se ejecutan periodicamente. Este específicamente se ejectuta:

    schedule:
      - cron: '0 12 * * *'

    Podes configurar tu periodo en esta página: Crontab.guru - The cron schedule expression editor

    El Job que aparece debajo llamado render_readme simplemente establece todos los pasos para:

    1. Montar una máquina con Ubuntu
    2. Cargar nuestro código
    3. Instalar Node.js en la máquina
    4. Instalar las dependencias que utiliza nuestro proyecto
    5. Ejecutar nuestro archivo de entrada index.js
    6. Si hay cambios, hacerles commit y push automático a nuestro repositorio (actualizarlo)

    Para que el paso número 5 pueda ejecutarse necesitamos proveerle la variable de entorno necesaria (RAPID_API_KEY en este caso) que no está disponible ya que no es información que enviamos a nuestro repositorio. Para añadirla vamos a seguir estos pasos:

    En la página de nuestro repositorio, nos dirigimos a: Settings > Secrets and Variables > Actions > New repository Secret y en esa pantalla rellenamos la información:


    Para el último paso, nuestra "GITHUB_TOKEN" va a tener que tener permisos para leer y escribir en nuestro repositorio. Para eso tenemos que ir a la configuración de nuestro repositorio y habilitarla Settings > Actions > General > Workflow Permissions > Read and write permissions:

    Este proceso también se dispara cuando nosotros hacemos un push a nuestro repositorio, es decir cada vez que nosotros mismos enviamos cambios:

    on:
      push:
        branches:
          - main

    Si tus rama tiene un nombre distinto, vas a tener que reemplazarlo en el lugar de main

    Enviando todo a nuestro repositorio

    Tan solo falta hacer commit y enviar todo a GitHub!

    $ git add .
    $ git commit -m "Initial script for automatic readme"
    $ git push origin main

    Este push va a disparar el workflow y vamos a poder ver cómo se ejecuta en nuestro repositorio, en la pestaña de Actions


    Así, por ejemplo se está generando y actualizando mi README a diario:

    Conclusión

    Y listo! 🎉🎉 Así es como creamos un readme dinámico y ✨ especial

    En la segunda parte de este post, vamos a completar la tabla de post, haciendo scrapping con puppeteer, una manera distinta para poder obtener información.

    Espero que este post te haya sido de ayuda o te haya dado nuevas ideas para crear tu propio programa. Si te gustó no dudes en dejar una estrellita en el repositorio ⭐

    • #javascript

    • #nodejs

    • #github