Trello Retro Refresher

Author: Nikita Hasis

Date: 2021-03-03

Tags: dev, program management, agile

I love Retros. And I run one every two weeks for each of my teams (and also for some other groups, like the iOS Code Owners guild).

We also love to keep it simple for our audience, so each team has a single recurring invite in Outlook, with a single, reusable Trello board for their retro. (Template link)

This link never changes, and it will probably outlive the Outlook invite it's on.

This means you can favorite your retro link and just get there, whenever you need. After every Retro, it becomes a new board, but the link stays the same.

Of course, this wizardry creates a little bit of manual labor tax for whomever organizes these.

After each retro, you (the organizer) will:

  • Save the state of the board for posterity (Make a Copy)
  • Rename the Perma-linked board to reflect the next retro's date.
  • Remove all the cards on the Perma-linked board, except for the Action Items (you have to follow up on those!)

It's not tear-your-hair-out work, but it easy to forget, and it's mundane enough to already make you feel like a robot. So why not just make a robot do it?

Enter Trello's (kind of not great) API!

There are 2 basic tasks:

  • Copy Board (simple, one call)
  • Refresh Original Board (a little more complicated, with some sequencing).

Making a copy of the existing board, with all its cards is easy, and documented.

// https://developer.atlassian.com/cloud/trello/rest/api-group-boards/#api-boards-post
const fetch = require('node-fetch');
const config = require('./config.json');
const boardData = {
name: "Test",
trello: "averylongidentifier",
};
fetch(`https://api.trello.com/1/boards/?key=${config.trello_key}&token=${config.trello_token}&name=${board_data.name}&idBoardSource=${board_data.id}&keepFromSource=cards`, { method: 'POST' });

idBoardSource: This will take either the ID of the original board you are copying. (The short/friendly ID will not do here, and you will embarrass yourself in front of your peers during a live demo.)

keepFromSource: Specifies whether cards should be carried over from the original board. The only other option here is none, which means...

name: is a mandatory field and there is no obvious way to inherit it from "Source". Which means you have to either make another call to get the board's name, or keep a local dictionary (referred to as boardData above).

Boring vs. ARGVs vs. Interactive

In the past I'd either go way overboard here (see the report-generator, that comes with a whole UI), or really cheap with hard coded values (see the basic-report-generator). Since we're working with at least three different groups here, on alternating schedules, I wanted flexibility. Having used ARGVs heavily back in the good old Ruby days, I was like Yes.

Then I realized I'd have to remember which arguments to pass, meaning I'd probably have to open my IDE. No way, man.

So I pulled up Inquirer. At around 500kb it is not small, considering the rest of the "app" is literally 68 lines. But we're about to pull in fns-date, so heck with it. It's fun and interactive.

Inquirer is going to read off some dictionary of common Trello boards I will need to clone, give those options to the user, and then pass the choices to the main function.

Neat!

await inquirer.prompt([
{
type: "list",
name: "team",
message: "Which Retro to Reset?",
choices: ["Android Team", "iOS Team", "Another iOS Team"],
},
{
type: "confirm",
name: "Confirm",
message: `This will refresh the retro!`,
},
]);

Putting in the Dictionary

Obviously you can shout your team’s name at the Trello API all day, it's not going to know what you want from it. So we assign some meaningful IDs to the Inquirer selections. We need to set up a basic data structure and import it into the Inquirer.prompt method as the "Choices".

exports.teams = [
{
name: "Test",
value: {
name: "Test",
trello: "averylongidentifier",
},
},
{
name: "iOS Team",
value: {
name: "iOS Team",
trello: "anotherverylongidentifier",
},
disabled: true,
},
...
]

Note the duplication; the top 'name' value is what is displayed by Inquirer. The name inside the Value is what gets returned as Answers or user's choices. I haven't found a clever way around this decidedly unclever implementation.

We form this dictionary to contain the two vital pieces of data: the Name of the board (used to create the new name) and the full, unique board identifier, import and wire up the options to Inquirer:

- c̶h̶o̶i̶c̶e̶s̶:̶ ̶[̶"A̶n̶d̶r̶o̶i̶d̶"̶,̶ ̶"̶i̶O̶S̶"̶]̶,̶
+ choices: dictionary.teams

When Is The Next Retro?

Typically, we run these bi-weekly. Sometimes that changes, but primarily, because there's nothing concrete or factual in the Trello Board's object about when this retro is happening, and the only indicator is in the name, and for the attendees convenience, it is always in the name (iOS Team Retro 1/2/2020, for example).

If only I could wire this to Outlook to see when the Retros are actually scheduled for... Well, that's a pipe dream for another chapter (or for you to solve in the comments). I guess I'll just do the cheap assumption that I remember to run this script at the end of a Retro, and thus the next Retro will be exactly two weeks from now.

So we get the date in what is probably a really inefficient way, and build out the Retro name string with the team.name we got from the User's answers and a bit of template text:

const date = new Date();
date.setDate(date.getDate() + 14);
const formattedDate = date.toLocaleDateString();
...
const retroName = team.name + " Retro - " + formattedDate

Testing

I set up a test board with four columns. Two of the columns will have cards that are purged from the refreshed board, and two columns that will not. (The idea here is that as part of the refresh, we want to keep the action items from retro to retro, for review and accountability. We can always manually archive a card when the action item has been completed.)

Then I broke things a lot and cleaned some code, and broke things some more. Something kept telling me to write tests, then someone kept telling me to write a blog and I picked this.

Eventually I was done testing it with the Test Board and decided to do it live; show off to the crew after a retro.

Then I did some more learning.

Risks

  • No Friendly IDs
    • My initial assumption (because I didn't read right) was that the friendly ID of a board (the one you get in the URL) was enough to run any of the operations. It is not. You must use the full identifier. I didn't do have these filled out correctly for my demo, using a short ID for my iOS group, and they were nice enough not to laugh at me, but things blew up.
  • Brittleness of String-based contracts
    • It would be awesome to set a "Target" date for a board entity. Instead we have to rely on Strings for facts, and that's brittle enough.

Conclusion

Trello's API is nice and underdeveloped, but it exists, we can connect to it, and it behaves pretty much as documented. I bet there are a thousand different ways you can use it to improve your day-to-day interactions with the tool. Or you can use Trello's Butler, but that too is another blog.

Want to use this? Here's some code. But please set up your own dictionary and use it with your own boards. The ACL part remains a TODO, and I don't want any surprises. (smile) Thanks!

PS: Inquirer is a tremendous amount of fun.

© 2021, Nikita Hasis