Get Magic Eight Ball answers right in your PR or issue comments!
Tags: githubhack23, node, chatgpt, jokes
Published: 2023-04-27 19:13:42

## What I built 

[Magic Eight Ball](https://en.wikipedia.org/wiki/Magic_8_Ball) is a wondrous device that lets a user offload any important decision to a plastic ball. Well, this one you don't even need to shake! I've built a GitHub Action that would allow you to tempt fate and receive answers to the most important questions right in the comments to a PR or an issue! 

### Category Submission: 

Wacky Wildcards

### App Link

[Try in action](https://github.com/ValeriaVG/actions-magic-8-ball/issues/1)


### Screenshots 
![Signs point to a nap first.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uf835jn7grnsh6y8gohx.png)

![My reply is no, but my heart says yes... to pizza.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4hi4v5fqrnzcqmrjni49.png)

### Description 

Action runs on each comment and if it starts with a `🎱` (or another defined prefix) - it will reply with a random answer.

### Link to Source Code 
{%embed https://github.com/ValeriaVG/actions-magic-8-ball %}

### Permissive License 
[MIT](https://github.com/ValeriaVG/actions-magic-8-ball/blob/main/LICENSE)

## Background

I wanted to build something magical, fun, a bit chaotic and over-the-top! I asked a magic eight ball if my fortune cookie idea was any good and it said that magic eight ball was a better one :)

### How I built it 

I've started with the actual magic eight ball script. The most important part was, of course, to write the answers. I've learned that a classic magic eight ball has 20 answers, but they are so boring...

I wanted something opposite of boring! Therefore I went to [ChatGPT](https://chat.openai.com/) and politely asked it to generate some funny responses:

![Generate 10 chaotic funny eight ball responses](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/brpufkrhcawxn4ufd84l.png)

Of course, I've asked it for an algorithm as well, but it was  too plain:
```js
const responses = [
  "It is certain",
  "Without a doubt",
  "Yes – definitely",
  "You may rely on it",
  "As I see it, yes",
  "Most likely",
  "Outlook good",
  "Yes",
  "Signs point to yes",
  "Reply hazy, try again",
  "Better not tell you now",
  "Cannot predict now",
  "Concentrate and ask again",
  "Don't count on it",
  "Outlook not so good",
  "My sources say no",
  "Very doubtful"
];

function generateEightBallResponse() {
  const randomIndex = Math.floor(Math.random() * responses.length);
  return responses[randomIndex];
}

// Example usage
console.log(generateEightBallResponse());
```

I've asked to rewrite it with `crypto.randomBytes`, because I sure wanted cryptographically secure random answers (who wouldn't? ):

```js
function generateEightBallResponse() {
  const randomBytes = crypto.randomBytes(1);
  const randomIndex = randomBytes[0] % responses.length;
  return responses[randomIndex];
}
```

This was a bit better, but one can never have enough magic and so I settled for the following overkill version ( writing it myself this time was faster than trying to explain to an AI what I had in mind ):

```js
function generateEightBallResponse() {
  const randomSize = crypto.randomBytes(1)[0];
  const randomValues = new Uint32Array(randomSize);
  crypto.getRandomValues(randomValues);
  const randomIndex = randomValues[Math.floor(Math.random() * randomSize)] % responses.length;
  return responses[randomIndex];
}
``` 

Now that's what I call magic! 

Next step was to turn it into an actual GitHub action.

I've looked for some similar actions and it was quite easy to replicate.

I've created an `action.yml` file in the root of my repository:
```yml
name: 'Magic Eight Ball'
branding:
  icon: 'message-circle'
  color: 'gray-dark'
description: 'Replies to a comment with a prediction'
inputs:
  prefix:
    description: 'Prefix that would trigger the response'
    default: '🎱'
    required: false
  GITHUB_TOKEN:
    description: 'Github token of the repository (automatically created by Github)'
    default: ${{ github.token }}
    required: false
runs:
  using: 'node16'
  main: 'lib/index.js'
```

Added the script to post a comment, using [@actions/core](https://www.npmjs.com/package/@actions/core) and [@actions/github](https://www.npmjs.com/package/@actions/github) packages:
```ts
import { context, getOctokit } from "@actions/github";
import { getInput } from "@actions/core";
import generateEightBallResponse from "./generateResponse";

const run = async () => {
  const prefix: string = getInput("prefix");
  const github_token: string = getInput("GITHUB_TOKEN");
  if (
    !context.payload.comment ||
    !context.payload.comment["body"]?.startsWith(prefix)
  ) {
    return;
  }
  const issue_number =
    context.payload.pull_request?.number || context.payload.issue?.number;
  if (!issue_number) {
    return;
  }

  const octokit = getOctokit(github_token);
  const body =
    '> ' + context.payload.comment["body"] + '\n\r' +
    "@" +
    context.payload.comment["user"].login +
    " " +
    generateEightBallResponse();
    
  await octokit.rest.issues.createComment({
    ...context.repo,
    issue_number,
    body,
  });
};

run();
```

And created a workflow to test this action in the same repository:
```yml
on:
  issue_comment:
    types: [created]
jobs:
  test_8ball:
    name: Magic Eight Ball
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      issues: write
      contents: read
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Run action from main
        uses: ./
```

It didn't work on the first try and I've learned about permissions (originally I was missing `contents: read` and my workflow was failing to checkout the repo).

Then, I've learned that default action runners support node16 at most and I couldn't use `crypto.getRandomValues` just yet. With a heavy heart I've settled for a little bit less magical random generator (by using `crypto.randomBytes` twice):

```js
export default function generateEightBallResponse() {
  const randomSize = crypto.randomBytes(1)[0];
  const randomValues = crypto.randomBytes(randomSize);
  const randomIndex =
    randomValues[Math.floor(Math.random() * randomSize)] % responses.length;
  return responses[randomIndex];
}
```

I could have used a different action and setup whatever version of node I needed, but I wanted the action to be as small and as fast as possible. 

I've bundled all code with [esbuild](https://esbuild.github.io/getting-started/) into one file and it finally it worked!

In the end I wanted to write some tests, but Magic eight ball told me not to :-P