Creating a Github Action that adds a Pokemon Sprite to your repo README
Tags: githubactions, pokemon, javascript
Published: 2022-12-29 19:36:47

**TL;DR**

Add a Pokemon Sprite to your repo READMEs for fun!

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ffax7d7m6d85fx23pa5b.png)

[Action](https://github.com/ibrahiem96/poke-readme-action)
[Example Usage](https://github.com/ibrahiem96/ibrahiem96)

This Christmas break, I had some time on my hands so I decided to write a Github Action that adds a fun pokemon sprite to your repo's README.md. I wrote this action in javascript using node-fetch, octokit/core, and actions/core. The sprite images are fetched from [PokeAPI](https://github.com/PokeAPI/pokeapi). I had a lot of fun learning more about javascript promises and Github Actions workflows.

---

## Acknowledgements

I'd like to shout out Athul for his work on [waka-readme](https://github.com/athul/waka-readme), which first made me realize that Github Actions could even be used in such a cool way.

I'd also like to shout out Geraldine for her [tutorial on how to update README files](https://blog.geraldiner.com/github-action-update-readmes-tutorial) with Github Actions.

Lastly, shout out to Github for making great documentation on how to [build an Action with Javascript](https://docs.github.com/en/actions/creating-actions/creating-a-javascript-action). 

## Getting Started

### Requirements
Install the following software:
1. Node16.x
2. npm

### Creating the project directory
First thing you'll need after installing node and npm is to actually setup the project files:

```bash
# create the project directory
mkdir poke-readme-action
cd poke-readme-action

# initialize node project install node modules
npm init
npm install --save node-fetch@v2 @actions/core @octokit/core @vercel/ncc
touch index.js
```

Now in `index.js` you will grab some test data from the PokeAPI.

```javascript
const fetch = require('node-fetch');
const pokemon = process.argv.slice(2)[0];
fetch(`https://pokeapi.co/api/v2/pokemon/${pokemon}`)
        .then((response) => response.json())
        .then((data) => { 
            console.log(data);
        });
```

Let's test this connection out:
```bash
node index.js pikachu
```

This prints a JSON data block (I've shortened the block so you can see the data you want--the pokemon sprite images):
```json
{
  ...
  sprites: {
    back_default: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/25.png',
    back_female: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/female/25.png',
    back_shiny: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/25.png',
    back_shiny_female: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/female/25.png',
    front_default: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/25.png',
    front_female: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/female/25.png',
    front_shiny: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/25.png',
    front_shiny_female: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/female/25.png',
  }
  ...
}
```

Now you can update the code to filter out just the sprite url from the rest of the data

```javascript
fetch(`https://pokeapi.co/api/v2/pokemon/${pokemon}`)
        .then((response) => response.json())
        .then((data) => { 
            const spriteUrl = data.sprites.front_default;
        });
```

## Javascript Promises 😖

One thing about Javascript that always eluded my understanding was the concept of Promises. As I sat down to figure out how to arrange the order of operations, I realized I needed to understand how Promises worked in order to create a proper data flow.

You know from the previous test that you can use node-fetch to retrieve a Promise object. Once retrieved, you can store it, but a Promise object in itself doesn't help much. You need to unpack the JSON data inside it, and that's where the then() function comes in.

But what's next? Do you pass the sprite data to our next set of operations? Do you store the sprite data asynchronously and call a different set of operations that use the stored sprite data?

Its worth experimenting just to learn how promises work, but for the sake of the blog post, I'll move ahead with what worked for me--to pass the sprite data on to the next set of operations (which I will need to define).

First thing's first: establish the order of operations

1. Fetch sprite url from PokeAPI
2. Create markdown snippet with sprite url
3. Get README.md file in repo
4. Add and commit new markdown snippet in repo

You already achieved step one previously, so let's hit the next 3!

## Setting up the connection to Octokit Core 

In order to make Github API calls, you have a few different libraries available, but I found octokit/core to be the easiest to work with and it naturally adheres to the REST API documentation. You'll also need actions/core to pull data from our Github Actions Workflow.

Let's set up the modules and some constants:

```javascript
const fetch = require('node-fetch');
const core = require('@actions/core');
const octocore = require("@octokit/core")

const pokemon = core.getInput('POKEMON');
const repo_owner = core.getInput('REPOSITORY_OWNER')
const gh_token = core.getInput('GH_TOKEN');
const commit_message = core.getInput('COMMIT_MESSAGE');
const repo_name = core.getInput('REPOSITORY');

const octocore_client = new octocore.Octokit({auth: gh_token})
```

The above inputs will all be defined later in our Actions YAML file, so for now all you need to know is that these variables will help us connect to the Github API and run some CRUD operations.

## Completing the Action

Now that you have our main piece of data (the pokemon sprite url), and our necessary inputs defined, let's get to work on completing the action code.

First you'll start by creating a function to get the README.md file from the repo:

```javascript
function getReadme(){
    return octocore_client.request(`GET /repos/${repo_owner}/${repo_name}/contents/README.md`)
}
```

The API call above constructs a get request to get the file given repo_name and repo_owner.

**Bug Alert**
Running this code will actually result in a bug due to the inputs.

The API call constructs the url as such: `/repos/ibrahiem96/ibrahiem96/ibrahiem96/contents/README.md`

This is because the repo_owner gets stored as: `ibrahiem96`, but the repo name gets stored as: `ibrahiem96/ibrahiem96`

So you need to add some code that can handle this exception:

```javascript
const repo = (repoName) => {
    if (repoName.includes('/')) {
        return repoName.split('/')[1]
    }
    else return repoName;
};

const repo_name = repo(core.getInput('REPOSITORY'));
```

This is a short lambda function that takes in the repo name input and returns the repository name after the slash (ex: `ibrahiem96/slybot` would return only `slybot`.

Now that you took care of this, our `getReadme()` function should work fine, and you can use it in our next function: `updateReadme()`.

The `updateReadme()` function will need to do the following things:

1. Write out the raw content of the README.md
3. Append the sprite url to the raw content README.md
4. Commit and push the new content

```javascript
function updateReadme(spriteMarkdown){
    getReadme().then(({ data }) => {
        const rawContent = Buffer.from(data.content, data.encoding).toString(); // step 1
        const startIndex = rawContent.indexOf("<!--Pokemon Sprite-->")
        const updatedContent = `${startIndex === -1 ? rawContent : rawContent.slice(0, startIndex)}\n${spriteMarkdown}` // step 2

        octocore_client.request(`PUT /repos/${repo_owner}/${repo_name}/contents/README.md`, {
            message: commit_message,
            content: Buffer.from(updatedContent, "utf-8").toString(data.encoding),
            path: "README.md",
            sha: data.sha,
        }) // step 3
    })
}
```

A couple of things to note about the function above:

- `const StartIndex` is a custom string that needs to be defined because that will tell the function where in the README.md to append our new content (the sprite url). Otherwise it will overwrite the entire file.
- When you get the file contents you decode from base64 to utf-8, and when you write the contents back you need to encode it in base64
- When editing existing file contents in github, the file sha is necessary. Otherwise Github will throw an error (rightfully so because it needs to know the exact version of the file to edit)

Finally you can call these functions inside our Fetch call:
```javascript
fetch(`https://pokeapi.co/api/v2/pokemon/${pokemon}`)
        .then((response) => response.json())
        .then((data) => { 
            const spriteUrl = data.sprites.front_default;
            const spriteMarkdown = `<!--Pokemon Sprite-->\n![image](${spriteUrl})`;

            updateReadme(spriteMarkdown);
        });
```

Now you're all done with the action code. [Click here to view the whole file](https://github.com/ibrahiem96/poke-readme-action/blob/main/index.js).

## Adding the Action YAML

The Action YAML file declares the inputs (that we defined in our javascript file earlier) and build engine (in this case, node16) for the action code. I'll spare you another long code block and just link an [example here](https://github.com/ibrahiem96/poke-readme-action/blob/main/action.yaml).

[Here's some more documentation on Action YAMLs](https://docs.github.com/en/actions/creating-actions/creating-a-javascript-action#creating-an-action-metadata-file).

## Testing End to End

### Build

Before you deploy to the main branch, you need to test the action end to end to see if it works! (Actually, you should have been testing all throughout).

Building the nodejs code requires all our dependencies. But [committing the node_modules folder in Github is generally agreed to be a bad practice](https://dev.to/grawl/why-you-dont-have-to-commit-nodemodules-folder-33nm).

Therefore, you will use a tool called `ncc` from `vercel`, which you installed in the beginning.

In your root directory, enter the following:
```bash
ncc build index.js --license licenses.txt
```

This will create a single gcc-like javascript file that the github runner's node16 build can run. [More on ncc here](https://github.com/vercel/ncc).

Once this is complete, you need to edit the `actions.yaml` so so that the action knows to look inside the `dist` folder that contains the compiled javascript code.

```yaml
runs:
  using: 'node16'
  main: 'dist/index.js'
```

### Setting up the "client"

In the repository you want to add the pokemon sprite to, you'll need to setup a workflow file and also add the markdown index to your README.md, which will let the action know where to inject the sprite url.

Your workflow file will be located at `<project-root>/.github/workflows/workflow-file.yaml`, and can look something like the following:

```yaml
name: Poke Readme

on:
  workflow_dispatch:
    inputs:
      pokemon:
        description: 'Pokemon Name'
        required: true
        default: 'pikachu'

jobs:
  add-poke-sprite:
    name: Add Pokemon Sprite to repo README
    runs-on: ubuntu-latest
    steps:
      - uses: ibrahiem96/poke-readme-action@main
        with:
          pokemon: ${{ inputs.pokemon }}

```

And here's an example of how to inject the index string in your README.md

```markdown
content
...
<!--Pokemon Sprite-->
...
content
```

To trigger the action, you need to go to your Actions Tab in the github repository and manually dispatch the workflow. You can run it with the default pikachu sprite, or you can check out the [list of all pokemon names supported by the API](https://github.com/PokeAPI/pokeapi/blob/master/data/v2/csv/pokemon.csv)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2x6fn28n4h4rt483wjz3.png)

Once this succeeds, you can see the sprite on your README.md.

This is my favorite pokemon, Salamence

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1rbvbbsfr2ne6hq7iafp.png)

## Cleanup and Deploy

That wraps up the project! What's left is to clean up the code, deploy, maybe version the project, and think about what enhancements or features to add in the future. 

See you next time, gotta catch 'em all !