Skip to content

Index

Coaching Corner Volume 4

Welcome to Cameron's Coaching Corner, where we answer questions from readers about leadership, career, and software engineering.

In this post, we'll look at a question posed by Bastien in the Engineering Manager's Slack Group on how to praise your team.

Context: New to Engineering Manager, managing 5 people and working in a 5 person team. My managees are not 100% on my team.

Details: OK, so I've quickly learnt how to spot mistakes and follow up improvements to both teams (one I manage and one I work on). I'm confident taking actions and communicating on all of that. But there is the other side -> congratulation and following up on behavior/action.

Example: The current team has low velocity. They recently finished the specs and review. It didn't happen for months (always late on that), but it's their "normal" velocity. I congratulated them, but I'm wondering if I should have since they "just did their job".

How do you congratulate your coworkers? Specifically

  • Do you? Why or Why Not?
  • How?
  • On trivial/exceptional stuff?
  • When and Where?

Having Coffee with Deno - Automating All the Things

Welcome to the final installment of our Deno series, where we build a script that pairs up people for coffee.

In the last post, we added the ability to post messages into a Slack channel instead of copying from a console window.

The current major problem is that we have to remember to run the script. We could always set up a cron job or scheduled task, however, what happens when we change machines? What if our computer stops working? What if someone else changes the script, how will we remember to get the latest and run it?

Having Coffee with Deno - Sharing the News

Welcome to the third installment of our Deno series, where we build a script that pairs up people for coffee.

In the last post, we're dynamically pulling members of the Justice League from GitHub instead of a hardcoded list.

Like any good project, this approach works, but now the major problem is that we have to run the script, copy the output, and post it into our chat tool so everyone knows the schedule.

It'd be better if we could update our script to post this message instead. In this example, we're going to use Slack and their incoming webhook, but you could easily tweak this approach to work with other tools like Microsoft Teams or Discord.

The Approach

In order to for the script to post to Slack, we'll need to make the following changes:

  1. Follow these docs from Slack to create an application and enable the incoming webhooks.
  2. Test that we can post a simple message to the channel
  3. From here, we'll need to add code to make a POST call to the webhook with a message
  4. Tweak the formatting so it looks nicer

Justice League Planning

Creating the Slack Application and creating the Webhook

For this step, we'll follow the instructions in the docs, ensuring that we're hooking it up to the right channel.

After following the steps, you should see something like the following:

Image of Slack App with Incoming Webhook

We can test that things are working correctly by running the curl command provided. If the message Hello World appears in the channel, congrats, you've got the incoming webhook created!

Modifying the Script to POST to Webhook

We have the Slack app created and verified that the incoming webhook is working, so we'll need to add this integration to our script.

Since we have this incoming webhook URL and Slack recommends treating this as a secret, we'll need to add this to our .env file.

Keep your webhook ULR safe image

.env
GITHUB_API_TOKEN="<yourTokenHere>"
SLACK_WEBHOOK="<yourWebHookHere>"

With this secret added, we can write a new function, sendMessage, that'll make the POST call to Slack. Since this is a new integration, we'll add a new file, slack.ts to put it in.

slack.ts
// Using axiod for the web connection
import axiod from "https://deno.land/x/axiod@0.26.2/mod.ts";

// function to send a message to the webhook
async function sendMessage(message: string): Promise<void> {
  // Get the webhookUrl from our environment
  const webhookUrl = Deno.env.get("SLACK_WEBHOOK")!;

  try {
    // Send the POST request
    await axiod.post(webhookUrl, message, {
      headers: {
        "Content-Type": "application/json",
      },
    });
  } catch (error) {
    // Error handling
    if (error.response) {
      return Promise.reject(
        `Failed to post message: ${error.response.status}, ${error.response.statusText}`
      );
    }
    return Promise.reject(
      "Failed for non status reason " + JSON.stringify(error)
    );
  }
}

export { sendMessage };

With sendMessage done, let's update index.ts to use this new functionality.

index.ts
import { load } from "https://deno.land/std@0.195.0/dotenv/mod.ts";
import {
  GetOrganizationMemberResponse,
  getMembersOfOrganization,
} from "./github.ts";
import { sendMessage } from "./slack.ts";
import { Pair, createPairsFrom, shuffle } from "./utility.ts";

await load({ export: true });

// Replace this with your actual organization name
const organizationName = "JusticeLeague";
const names = await getMembersOfOrganization(organizationName);
const pairs = createPairsFrom(shuffle(names));
const message = createMessage(pairs);
// Slack expects the payload to be an object of text, so we're doing that here for now
await sendMessage(JSON.stringify({ text: message }));

function createMessage(pairs: Pair<GetOrganizationMemberResponse>[]): string {
  const mapper = (p: Pair<GetOrganizationMemberResponse>): string =>
    `${p.first.login} meets with ${p.second.login}${
      p.third ? ` and ${p.third.login}` : ""
    }`;
  return pairs.map(mapper).join("\n");
}

And if we were to run the above, we can see the following message get sent to Slack.

Message from Random Coffee

Nice! We could leave it here, but we could make the message prettier (having an unordered list and italicizing names), so let's work on that next.

Pretty Printing the Message

So far, we could leave the messaging as is, however; it's a bit muddled. To help it pop, let's make the following changes.

  • Italicize the names
  • Start each pair with a bullet point

Since Slack supports basic Markdown in the messages, we can use the _ for italicizing and - for the bullet points. So let's modify the createMessage function to add this formatting.

index.ts
function createMessage(pairs: Pair<GetOrganizationMemberResponse>[]): string {
  // Let's wrap each name with the '_' character
  const formatName = (s: string) => `_${s}_`;

  const mapper = (p: Pair<GetOrganizationMemberResponse>): string =>
    // and start each pair with '-'
    `- ${formatName(p.first.login)} meets with ${formatName(p.second.login)}${
      p.third ? ` and ${formatName(p.third.login)}` : ""
    }`;
  return pairs.map(mapper).join("\n");
}

By making this small change, we now see the following message:

Formatted Slack Message with italics and bullets

The messaging is better, but we're still missing some clarity. For example, what date is this for? Or what's the purpose of the message? Looking through these docs, it seems like we could add different text blocks (like titles). So let's see what this could look like.

One design approach is to encapsulate the complex logic for dealing with Slack and only expose a "common-sense" API for consumers. In this regard, I think using a Facade pattern would make sense.

We want to expose the ability to set a title and to set a message through one or more lines of text. Here's what that code would look like

slack.ts
// This class allows a user to set a title and lines and then use the
// 'build' method to create the payload to interact with Slack

class MessageFacade {
  // setting some default values
  private header: string;
  private lines: string[];
  constructor() {
    this.header = "";
    this.lines = [];
  }

  // I like making these types of classes fluent
  // so that it returns itself.
  public setTitle(title: string): MessageFacade {
    this.header = title;
    return this;
  }
  public addLineToMessage(line: string | string[]): MessageFacade {
    if (Array.isArray(line)) {
      this.lines.push(...line);
    } else {
      this.lines.push(line);
    }
    return this;
  }

  // Here's where we take the content that the user provided
  // and convert it to the JSON shape that Slack expects
  public build(): string {
    // create the header block if set, otherwise null
    const headerBlock = this.header
      ? {
          type: "header",
          text: { type: "plain_text", text: this.header, emoji: true },
        }
      : null;
    // convert each line to it's own section
    const lineBlocks = this.lines.map((line) => ({
      type: "section",
      text: { type: "mrkdwn", text: line },
    }));
    return JSON.stringify({
      // take all blocks that have a value and set it here
      blocks: [headerBlock, ...lineBlocks].filter(Boolean),
    });
  }
}

With the facade in place, let's look at implementing this in index.ts

index.ts
1
2
3
4
5
6
7
8
9
// ... code to get the pairs and formatted lines

// using the facade with the fluent syntax
const message = new MessageFacade()
  .setTitle(`☕ Random Coffee ☕`)
  .addLineToMessage(formattedPairs)
  .build();

await sendMessage(message);

When we run the script now, we get the following message:

Random Coffee Message with Header and Icons

Wrapping Up

In this post, we changed our script from posting its Random Coffee message to the console window to instead posting it into a Slack channel using an Incoming Webhook. By making this change, we were able to remove a manual step (e.g., us copying the message into the channel), and we were able to use some cool emojis and better formatting.

In the final post, we'll take this one step further by automating the script using scheduled jobs via GitHub Actions.

As always, you can find a full working version of this bot on my GitHub.

Scaling Effectiveness with Docs

As a leader, I'm always looking for ways to help my team to be more efficient. To me, an efficient team is self-sufficient, able to find the information needed to solve their problems.

I've found that having up-to-date documentation is critical for a team because it scales out knowledge in asynchronously, removing the need for manual knowledge transfers.

For example, my team has a wiki that contains information for onboarding into our space, how to complete certain processes (requesting time off, resetting a password), how to run our Agile activities, and our support guidebook. At any point, if someone on the team doesn't know how to do something, they can consult the wiki and find the necessary information.

Docs. Why Did It Have to Be Docs?

I enjoy up-to-date documentation, but the main problem with them is that they captured the state of the world when they were written, but they don't react to changes. If the process for resetting your password changes, the documentation doesn't auto-update. So unless you're spending time reviewing the docs, they'll grow stale and be worthless, or even worse, mislead others to do the wrong things.

A good mental model for documentation is to think of them as a garden. When planted, it looks great, and everyone enjoys the environment. Over time, weeds will grow, and plants will become overgrown, causing the garden to be less attractive. Eventually, people will stop visiting, and the garden will go into disrepair. To prevent this, we must take care of the garden, removing the weeds and trimming the plants.

Outdoor green space

Photo by Robin Wersich via Unsplash.com

Alright, I get it, documentation is important, but my team has commitments, so how do we carve out time to review?

Cameron Learns About Document Control

I started my career in healthcare, and one of my first jobs was writing software for a medical diagnostic device. We were ISO 9001 certified, and the device was considered a Class II from the FDA. Long story short, this meant that we have to provide documentation for our device and software and also show that we were keeping things up to date.

To comply, we would find docs that hadn't been updated in a specific time period (like 90 days) and review them. If everything checked out, we'd bump up the review date. Otherwise, we'd make the necessary changes and revalidate the document.

At the time, all of our files were in Word, so it wasn't the easiest to search them (I recall that we had Outlook reminders, but this was many moons ago).

By baking this into our process, this helped make our work more visible, which in turn, gave us a better idea of the team's capacity for that sprint.

Thankfully, we have better technology than Word for sharing information, so how can we take this approach and bring it up to the modern day?

Modern Take on an Old Classic

First, I think that having your docs in source control is a great idea. If you're using tools like Git, you already have a way of leaving comments and keeping track of approvals through pull requests.

To make the most of Git, you should keep your changes in plaintext as it's easy to see the differences. and I enjoy using Markdown and tools like Mkdocs make this workflow possible.

With this figured out, our next step is to know when the file was last reviewed. We can do that by adding a new line to the bottom of each file, Last Reviewed On: YYYY/MM/DD. To come up with the initial date, we could use the last time the file was modified (thanks git log!).

At this point, we have a way to see the last time the file was reviewed, next step is to write a script that can find files that haven't been reviewed in the last 90 days. At a high level, we'd do the following:

  1. Get the latest for the doc repository.
  2. Get all the markdown files for the repository.
  3. Get the last line of the file.
  4. If the line doesn't start with Last Reviewed On:, we flag it for review as it's never been reviewed.
  5. If the line has a date, but it's older than 90 days, we flag it for review as it might be stale.
  6. Print all flagged files to the screen.

With the script created, we could manually run this on Mondays. But we're technical, right? Why not create a scheduled task to execute this script instead? This removes a manual task to be ran and it gives us visibility on what docs need reviewed.

Wrapping Up

When scaling your knowledge out, having great documentation is necessary as it allows your team to self-serve and work in a more asynchronous manner. The main problem with documentation is that it captures the state of the world when the docs were written, but they don't automatically update when the world changes.

Therefore, we need to have some process to flag and review stale docs. To ensure it gets done, we provide visibility by creating work items and committing to them during the sprint.

Having Coffee with Deno - Dynamic Names

Welcome to the second installment of our Deno series, where we build a script that pairs up people for coffee.

In the last post, we built a script that helped the Justice League meet up for coffee.

As of right now, our script looks like the following.

index.ts
const names = [
  "Batman",
  "Superman",
  "Green Lantern",
  "Wonder Woman",
  "Static Shock", // one of my favorite DC heroes!
  "The Flash",
  "Aquaman",
  "Martian Manhunter",
];
const pairs = createPairsFrom(shuffle(names));
const message = createMessage(pairs);
console.log(message);

function shuffle<T>(items: T[]): T[] {
  const result = [...items];
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * i);
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
}
type Pair<T> = { first: T; second: T; third?: T };
function createPairsFrom<T>(items: T[]): Pair<T>[] {
  if (items.length < 2) {
    return [];
  }
  const results = [];
  for (let i = 0; i <= items.length - 2; i += 2) {
    const pair: Pair = { first: items[i], second: items[i + 1] };
    results.push(pair);
  }
  if (items.length % 2 === 1) {
    results[results.length - 1].third = items[items.length - 1];
  }
  return results;
}
function createMessage(pairs: Pair<string>[]): string {
  const mapper = (p: Pair<string>) =>
    `${p.first} meets with ${p.second}${p.third ? ` and ${p.third}` : ""}`;

  return pairs.map(mapper).join("\n");
}

Even though this approach works, the major problem is that every time there's a member change in the Justice League (which seems to happen more often than not), we have to go back and update the list manually.

It'd be better if we could get this list dynamically instead. Given that the League are great developers, they have their own GitHub organization. Let's work on integrating with GitHub's API to get the list of names.

The Approach

To get the list of names from GitHub, we'll need to do the following.

  1. First, we need to figure out which GitHub endpoint will give us the members of the League. This, in turn, will also tell us what permissions we need for our API scope.
  2. Now that we have a secret, we need to update our script to read from an .env file.
  3. Once we have the secret being read, we can create a function to retrieve the members of the League.
  4. Miscellaneous refactoring of the main script to handle a function returning complex types instead of strings.

Justice League Planning

Laying the Foundation

Before we start, we should reactor our current file. It works, but we have a mix of utility functions (shuffle and createPairsFrom) combined with presentation functions (createMessage). Let's go ahead and move shuffle and createPairsFrom to their own module.

utility.ts
type Pair<T> = { first: T; second: T; third?: T };

function shuffle<T>(items: T[]): T[] {
  const result = [...items];
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * i);
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
}

function createPairsFrom<T>(items: T[]): Pair<T>[] {
  if (items.length < 2) {
    return [];
  }
  const results: Pair<T>[] = [];
  for (let i = 0; i <= items.length - 2; i += 2) {
    const pair: Pair<T> = { first: items[i], second: items[i + 1] };
    results.push(pair);
  }
  if (items.length % 2 === 1) {
    results[results.length - 1].third = items[items.length - 1];
  }
  return results;
}

export { createPairsFrom, shuffle };
export type { Pair };

With these changes, we can update index.ts to be:

index.ts
import { Pair, createPairsFrom, shuffle } from "./module.ts";

const names = [
  "Batman",
  "Superman",
  "Green Lantern",
  "Wonder Woman",
  "Static Shock", // one of my favorite DC heroes!
  "The Flash",
  "Aquaman",
  "Martian Manhunter",
];
const pairs = createPairsFrom(shuffle(names));
const message = createMessage(pairs);
console.log(message);

function createMessage(pairs: Pair<string>[]): string {
  const mapper = (p: Pair<string>) =>
    `${p.first} meets with ${p.second}${p.third ? ` and ${p.third}` : ""}`;

  return pairs.map(mapper).join("\n");
}

Getting GitHub

Now that our code is more tidy, we can focus on figuring out which GitHub endpoint(s) to use to figure out the members of the Justice League.

Taking a look at the docs, we see that there are two different options.

  1. Get members of an Organization
  2. Get members of a Team

What's the difference between the two? In GitHub parlance, an Organization is an overarching entity that consists of members which, in turn, can be part of multiple teams.

Using the Justice League as an example, it's an organization that contains Batman, and Batman can be part of the Justice League Founding Team and a member of the Batfamily Team.

Since we want to pair everyone up in the Justice League, we'll use the Get members of an Organization approach.

Working with Secrets

To interact with the endpoint, we'll need to create an API token for GitHub. Looking over the docs, our token needs to have the read:org scope. We can create this token by following the instructions here about creating a Personal Auth Token (PAT).

Once we have the token, we can invoke the endpoint with cURL or Postman to verify that we can communicate with the endpoint correctly.

After we've verified, we'll need a way to get this API token into our script. Given that this is sensitive information, we absolutely should NOT check this into the source code.

Creating an ENV File

A common way of dealing with that is to use an .env file which doesn't get checked in, but our application can use it during runtime to get secrets.

Let's go ahead and create the .env file and put our API token here.

.env
GITHUB_BEARER_TOKEN="INSERT_TOKEN_HERE"

Our problem now is that if we check git status, we'll see this file listed as a change. We don't want to check this in, so let's add a .gitignore file.

Adding a .gitignore File

With the .env file created, we need to create a .gitignore file, which tells Git not to check in certain files.

Let's go ahead and add the file. You can enter the below, or you can use the Node gitignore file (found here)

.gitignore
.env # ignores all .env files in the root directory

We can validate that we've done this correctly if we run git status and don't see .env showing up anymore as a changed file.

Loading Our Env File

Now that we have the file created, we need to make sure that this file loads at runtime.

In our index.ts file, let's make the following changes.

index.ts
1
2
3
4
5
6
7
8
9
import { config as loadEnv } from "https://deno.land/x/dotenv@v3.2.2/mod.ts";
// other imports

// This loads the .env file and adds them to the environment variable list
await loadEnv({ export: true });
// Deno.env.get("name") retrieves the value from an environment variable named "name"
console.log(Deno.env.get("GITHUB_BEARER_TOKEN"));

// remaining code

When we run the script now with deno run, we get an interesting prompt:

Deno requests read access to ".env".
- Requested by `Deno.readFileSync()` API.
- Run again with --allow-read to bypass this prompt
- Allow?

This is one of the coolest parts about Deno; it has a security system that prevents scripts from doing something that you hadn't intended through its Permissions framework.

For example, if you weren't expecting your script to read from the env file, it'll prompt you to accept. Since packages can be taken over and updated to do nefarious things, this is a terrific idea.

The permissions can be tuned (e.g., you're only allowed to read from the .env file), or you can give blanket permissions. In our cases, two resources are being used (the ability to read the .env file and the ability to read the GITHUB_BEARER_TOKEN environment variable).

Let's run the command with the allow-read and allow-env flags.

deno run --allow-run --allow-env ./index.ts

If the bearer token gets printed, we've got the .env file created correctly and can proceed to the next step.

Let's Get Dynamic

Now that we have the bearer token, we can work on calling the GitHub Organization endpoint to retrieve the members.

Since this is GitHub related, we should create a new file, github.ts, to host our functions and types.

Adding axiod

In the github.ts file, we're going to be use axiod for communication. It's similar to axios in Node and is better than then the built-in fetch API.

Let's go ahead and bring in the import.

github.ts
import axiod from "https://deno.land/x/axiod@0.26.2/mod.ts";

Calling the Organization Endpoint

With axiod pulled in, let's write the function to interact with the GitHub API.

github.ts
// Brining in the axiod library
import axiod from "https://deno.land/x/axiod@0.26.2/mod.ts";

async function getMembersOfOrganization(orgName: string): Promise<any[]> {
  const url = `https://api.github.com/orgs/${orgName}/members`;
  // Necessary headers are found on the API docs
  const headers = {
    Accept: "application/vnd.github+json",
    Authorization: `Bearer ${Deno.env.get("GITHUB_BEARER_TOKEN")}`,
    "X-GitHub-Api-Version": "2022-11-28",
  };

  try {
    const resp = await axiod.get<any[]>(url, {
      headers: headers,
    });
    return resp.data;
  } catch (error) {
    // Response was received, but non-2xx status code
    if (error.response) {
      return Promise.reject(
        `Failed to get members: ${error.response.status}, ${error.response.statusText}`
      );
    } else {
      // Response wasn't received
      return Promise.reject(
        "Failed for non status reason " + JSON.stringify(error)
      );
    }
  }
}

To prove this is working, we can call this function in the index.ts file and verify that we're getting a response.

index.ts
1
2
3
4
5
6
7
8
9
import { config as loadEnv } from "https://deno.land/x/dotenv@v3.2.2/mod.ts";
import { getMembersOfOrganization } from "./github.ts";
import { Pair, createPairsFrom, shuffle } from "./utility.ts";

await loadEnv({ export: true });

const membersOfOrganization = await getMembersOfOrganization("JusticeLeague");
console.log(JSON.stringify(membersOfOrganization));
// rest of the file

Now let's rerun the script.

deno run --allow-read --allow-env ./main.ts
Deno requests net access to "api.github.com"
- Requested by `fetch` API.
- Run again with --allow-net to bypass this prompt.

Ah! Our script is now doing something new (making network calls), so we'll need to allow that permission by using the --allow-net flag.

deno run --allow-read --allow-env --allow-net ./main.ts

If everything has worked, you should see a bunch of JSON printed to the screen. Success!

Creating the Response Type

At this point, we're making the call, but we're using a pesky any for the response, which works, but it doesn't help us with what properties we have to work with.

Looking at the response schema, it seems the main field we need is login. So let's go ahead and create a type that includes that field.

github.ts
type GetOrganizationMemberResponse = {
  login: string;
};

async function getMembersOfOrganization(
  orgName: string
): Promise<GetOrganizationMemberResponse[]> {
  //code
  const resp = await axiod.get<GetOrganizationMemberResponse[]>(url, {
    headers: headers,
  });
  // rest of the code
}

We can rerun our code and verify that everything is still working, but now with better type safety.

Cleaning Up

Now that we have this function written, we can work to integrate it with our index.ts script.

index.ts
import { config as loadEnv } from "https://deno.land/x/dotenv@v3.2.2/mod.ts";
import { getMembersOfOrganization } from "./github.ts";
import { Pair, createPairsFrom, shuffle } from "./utility.ts";

await loadEnv({ export: true });

const names = await getMembersOfOrganization("JusticeLeague");
const pairs = createPairsFrom(shuffle(names));
const message = createMessage(pairs);
console.log(message);

So far, so good. The only change we had to make was to replace the hardcoded array of names with the call to getMembersOfOrganization.

Not an issue, right?

Hmmm, what's up with this? createMessage has a type error

It looks like createMessage is expecting Pair<string>[], but is receiving Pair<GetOrganizationMemberResponse>[].

To solve this problem, we'll modify createMessage to work with GetOrganizationMemberResponse.

index.ts
// Need to update the input to be Pair<GetOrganizationMemberResponse>
function createMessage(pairs: Pair<GetOrganizationMemberResponse>[]): string {
  // Need to update mapper function to get the login property
  const mapper = (p: Pair<string>): string =>
    `${p.first.login} meets with ${p.second.login}${
      p.third ? ` and ${p.third.login}` : ""
    }`;

  return pairs.map(mapper).join("\n");
}

With this last change, we run the script and verify that we're getting the correct output, huzzah!

Current Status

Congratulations! We now have a script that is dynamically pulling in heroes from the Justice League organization instead of always needing to see if Green Lantern is somewhere else or if another member of Flash's SpeedForce is here for the moment.

A working version of the code can be found on GitHub.

Coaching Corner Volume 3

Welcome to Cameron's Coaching Corner, where we answer questions from readers about leadership, career, and software engineering.

In this week's post, we look at how Alan can help their engineer figure out what they want to be when they grow up.

Hey Cameron!

I have a front-end engineer who's sharp, but they're not sure what their career growth looks like. I get the sense that they're interested in other roles outside of software development. How do you navigate this and help them grow?

Having Coffee with Deno - Inspiration

In a previous post, I mention my strategy of building relationships through one-on-ones. One approach in the post was leveraging a Slack plugin, Random Coffee, to automate scheduling these impromptu conversations.

Dinosaur sitting in a coffee cup

I wanted to leverage the same idea at my current company; however, we don't use Slack, so I can't just use that bot.

High-Level Breakdown

Thinking more about it, the system wouldn't be too complicated as it has three moving parts:

  • Get list of people
  • Create random pairs
  • Post message

To make it even easier, I could hardcode the list of people, and instead of posting the message to our message application, I could print it to the screen.

With these two choices made, I would need to build something that can shuffle a list and create pairs.

Technology Choices

Even though we're hardcoding the list of names and printing a message to the screen, I know that the future state is to get the list of names dynamically, most likely through an API call. In addition, most messaging systems support using webhooks to create a message, so that would be the future state as well.

With these restrictions in mind, I know I need to use a language that is good at making HTTP calls. I also want this automation to be something that other people outside of me can maintain, so if I can use a language that we typically use, that makes this more approachable.

In my case, TypeScript fit the bill as we heavily use it in my space, the docs are solid, and it's straightforward to make HTTP calls. I'm also a fan of functional programming, which TypeScript supports nicely.

My major hurdle at this point is that I'd like to execute this single file of TypeScript, and the only way I knew how to do that was by spinning up a Node application and using something like ts-node to execute the file.

Talking to a colleague, they recommended I check out Deno as a possible solution. The more I learned about it, the more I thought this would fit perfectly. It supports TypeScript out of the box (no configuration needed), and a file can be ran with deno run, no other tools needed.

This project is simple enough that if Deno wasn't a good fit, I could always go back to Node.

With this figured out, we're going to create a Deno application using TypeScript as our language of choice.

Getting Started With Deno

  1. Install Deno via these instructions
  2. Setup your dev environment - I use VS Code, so adding the recommended extension was all I needed.

Trying Deno Out

Once Deno has been installed and configured, you can run the following script and verify everything is working correctly. It creates a new directory called deno-coffee, writes a new file and executes it via deno.

1
2
3
4
mkdir deno-coffee
cd deno-coffee
echo 'console.log("Hello World");' >> coffee.ts
deno run coffee.ts

We've got something working, so let's start building out the random coffee script.

Let's Get Percolating

As mentioned before, we're going to hardcode a list of names and print to the screen, so let's build out the rough shape of the script:

const names = [
  "Batman",
  "Superman",
  "Green Lantern",
  "Wonder Woman",
  "Static Shock", // one of my favorite DC heroes!
  "The Flash",
  "Aquaman",
  "Martian Manhunter",
];
const pairs = createPairsFrom(shuffle(names));
const message = createMessage(pairs);
console.log(message);

This code won't compile as we haven't defined what shuffle, createPairsFrom, or createMessage does, but we can tackle these one at a time.

Let's Get Random

Since we don't want the same people meeting up every time, we need a way to shuffle the list of names. We could import a library to do this, but what's the fun in that?

In this case, we're going to implement the Fisher-Yates Shuffle (sounds like a dance move).

function shuffle(items: string[]): string[] {
  // create a copy so we don't mutate the original
  const result = [...items];
  for (let i = result.length - 1; i > 0; i--) {
    // create an integer between 0 and i
    const j = Math.floor(Math.random() * i);
    // short-hand for swapping two elements around
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
}

const words = ["apples", "bananas", "cantaloupes"];
console.log(shuffle(words)); // [ "bananas", "cantaloupes", "apples" ]

Excellent, we have a way to shuffle. One refactor we can make is to have shuffle be generic as we don't care what array element types are, as long as we have an array.

Making this refactor gives us the following:

1
2
3
4
5
6
7
8
function shuffle<T>(items: T[]): T[] {
  const result = [...items];
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * i);
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
}

Now, we can shuffle an array of anything. Nice!

Two of a Kind

Let's take a look at the next function, createPairsFrom. We know its type signature is going fromstring[] to something, but what?

In the ideal world, our total list of names is even, so we always have equal pairs.

1
2
3
4
{first: 'Batman', second: 'Superman'},
{first: 'Green Lantern', second: 'Wonder Woman'},
{first: 'Static Shock', second: 'The Flash'},
{first: 'Aquaman', second: 'Martian Manhunter'}

But what happens if Martian Manhunter is called away and isn't available? That would leave Aquaman without a pair to have coffee with (sad trombone noise).

In the case that we have an odd number of heroes, the last pair should instead be a triple which would look like the following:

1
2
3
{first: 'Batman', second: 'Superman'},
{first: 'Green Lantern', second: 'Wonder Woman'},
{first: 'Static Shock', second: 'The Flash', third: 'Martian Manhunter'}

Given that we've been using the word Pair to represent this grouping, we have a domain term we can use. This also means that createPairsFrom has the following type signature.

1
2
3
function createPairsFrom(names: string[]): Pair[] {
  // logic
}

But what does the Pair type look like? We can model either using an optional property or by using a discriminated union.

// Using an optional property
type Pair {
  first: string,
  second: string,
  third?: string
}

// Using Discriminated Unions
type Pair = {kind: 'double', first:string, second: string}
          | {kind: 'triple', first:string, second: string; third: string}

For now, I'm thinking of going with the optional property and if we need to tweak it later, we can.

Let's go ahead and implement createPairsFrom.

function createPairsFrom(names: string[]): Pair[] {
  // if we don't have at least two names, then there are no pairs
  if (names.length < 2) {
    return [];
  }
  const results = [];
  for (let i = 0; i <= names.length - 2; i += 2) {
    const pair: Pair = { first: names[i], second: names[i + 1] };
    results.push(pair);
  }
  if (names.length % 2 === 1) {
    // we have an odd length
    // Assign the left-over name to the third of the triple
    results[results.length - 1].third = names[names.length - 1];
  }
  return results;
}

// Example execution
console.log(createPairsFrom(["apples", "bananas", "cantaloupes", "dates"])); // [{first:"apples", second:"bananas"}, {first:"cantaloupes", second:"dates"}]
console.log(createPairsFrom(["ants", "birds", "cats"])); // [{first:"ants", second:"birds", third:"cats"}]

Similarly to shuffle, we can make this function generic as it doesn't matter what the array element types are, as long as we have an array to work with.

Refactoring to generics gives us the following:

1
2
3
4
5
type Pair<T> = { first: T; second: T; third?: T };

function createPairsFrom<T>(items: T[]): Pair<T>[] {
  // same function as before
}

To the Presses!

For the last part, we need to implement createMessage. We know it has to have the following type signature:

function createMessage(pairs: Pair<string>[]): string {}

We know the following rules.

  • When it's a double, we want the message to say, X meets with Y
  • When it's a triple, we want the message to say, X meets with Y and Z

Based on this, we need a way to map from Pair to the above string. So let's write that logic.

1
2
3
4
5
6
function createMessage(pairs: Pair<string>[]): string {
  const mapper = (p: Pair<string>) =>
    `${p.first} meets with ${p.second}${p.third ? ` and ${p.third}` : ""}`;

  pairs.map(mapper);
}

From here, we can join the strings together using the \n (newline) character.

1
2
3
4
5
6
function createMessage(pairs: Pair<string>[]): string {
  const mapper = (p: Pair<string>) =>
    `${p.first} meets with ${p.second}${p.third ? ` and ${p.third}` : ""}`;

  return pairs.map(mapper).join("\n");
}

All Coming Together

With the implementation of createMessage, we can execute our script by running deno run coffee.ts

1
2
3
4
5
6
deno run coffee.ts

"Superman meets with Wonder Woman
Batman meets with The Flash
Martian Manhunter meets with Aquaman
Static Shock meets with Green Lantern"

From here, we have a working proof of concept of our idea. We could run this manually on Mondays and then post this to our messaging channel (though you might want to switch the names out). If you wanted to be super fancy, you could have this scheduled as a cron job or through Windows Task Scheduler.

The main takeaway is that we've built something we didn't have before and can continue to refine and improve. If no one likes the idea, guess what? We only had a little time invested. If it takes off, then that's great; we can spend more time making it better.

Wrapping Up

In this post, we built the first version of our Random Coffee script using TypeScript and Deno. We focused on getting our tooling working and building out the business rules for shuffling and creating the pairs.

In the next post, we'll look at making this script smarter by having it retrieve a list of names dynamically from GitHub's API!

As always, you can find a full working version of this bot on my GitHub.

Cameron's Coaching Corner Volume 2

Welcome to Cameron's Coaching Corner, where we answer questions from readers about leadership, career, and software engineering.

In this week's post, we look at how Chase can balance writing the perfect code and shipping something.

My question: As a young developer, I notice that sometimes I get paralyzed by options. I want to write the perfect piece of code. This helps me in writing good code but usually at the cost of efficiency. Especially when I am faced with multiple good options. Sometimes I want to KNOW I’m gonna write the right thing before I’m writing it when I my be better off with some trial and error

  1. Are these common problems that you see people face?
  2. What rules of thumb or other pieces of advice do you have to avoid writing nothing instead of something as a result of seeking the ideal?
  3. How important is planning vs trial and error ("failing fast" as they say) to good software development flow?

Five Minutes at Five Guys - When Metrics Conflict with UX

In a recent post, I spoke about the flaw of using a single metric to tell the story and how Goodhart's Law tells us that once we start measuring a metric, it stops being a useful metric.

Let's look at a real-world example with the popular fast food chain, Five Guys.

All I Wanted Was a Burger

Five Guys is known for making good burgers and delivering a mountain of piping hot fries as part of your order. Seriously, an order of small fries is a mountain of spuds. Five Guys make their fries to order, so they're not sitting around under a heat lamp.

Yo Dawg, I heard you wanted fries, so I put fries in your fries

This approach works great when ordering in person, but what happens if you order online? The process is essentially the same, the crew works on the burgers, but they won't start the fries until you're at the restaurant, so they're always guaranteeing that you get fresh made fries.

At this point, it's clear that receiving a mountain of hot, cooked-to-order fries is part of the experience and what customers expect, right?

Cameron's Coaching Corner - Mentoring an Intern

Welcome to Cameron's Coaching Corner, where we answer questions from readers about leadership, career, and software engineering.

In this week's post, we look at how test123 can improve the mentoring experience for their new intern.

I recently had an intern join my time and I’m going to be his mentor. I’ve had interns in the past, but this one doesn’t understand any fundamentals and struggles with everything.

My question to you is this, how can I help him? He doesn’t know HTML/CSS/JS, so I’m trying to teach him those, but it’s taking away a lot of time. I suggested for him to watch some videos and then we can sync twice a day to go over the topics and discuss them further.

My issue: I don’t want to just say “go watch videos.” Bc, that’s not the best way to learn - I want him to dive into the code and try things and break, that’s how I learned at least.

How do you think I should handle this? I wanna be a good mentor and I want him to learn and grow. I don’t wanna fail the kid bc I don’t know the proper way to mentor.