I am now published! (in NPM)
I published by first NPM package yesterday!
It all started when I was pushing some commits to GitHub that other people had helped me on, but since I wasn’t creating the commit from a pull request, the Co-authored-by
commit trailer wasn’t generated automatically.
In the case that you don’t know, the format for this trailer is:
Co-authored-by: [user full-name] <[user email]>
And neither of those attributes are easily found if you don’t already know them. You have to click through github profile pages, copy and pasting precariously.
So as a fun example I decided to make an app to do this for me. It had the following requirements:
- Had to be easy, I just wanted to type in the github user name (e.g.
neenjaw
) and then get a string back (e.g.Co-authored-by: Tim Austin <tim@neenjaw.com>
) - It had to be live, pulling the data from github using its rest-api interface
- It had to be secure, not storing my github personal access token in plain text
From those, the most difficult one seemed to be the third – securely handling my github personal access token.
Loose Lips Sink Ships
A keychain is the perfect place for something like a personal access token and I do most (if not all) development in Ubuntu (either Ubuntu proper or WSL2 Ubuntu), so I knew about the libsecret interface for storing secrets using DBUS channels. I had never interacted with it before directly. It is relatively straight forward though. You can either use C-lang bindings, or instead you can use libsecret-tools
to get a command-line tool to interact with it.
From there using secret-tool
you can store a secret (echo "secret" | secret-tool store --label="your label" {attribute} {value}
), retrieve a secret (secret-tool lookup {attribute} {value}
), or clear it (secret-tool clear {attribute} {value}
). So using nodejs, this is as easy as executing the command string in a child-precess and running each command:
const { exec } = require('child_process')
// Getting a secret from libsecret
exec(`secret-tool lookup {attribute} {value}`, (error, stdout, stderr) => {
if (error) {
// Handle the error
return
}
if (stderr) {
// Handle the stderr output
return
}
// use the stdout output
})
This is a pretty standard nodejs pattern, an asynchronous function, taking a callback to then handle the result whenever the process should finish. They are pretty easy to use, but over time they produce a lot of complexity and deeply nested structures – a.k.a. Callback Hell. So just for fun I wanted to wrap this into a javascript Promise
and then practice using ES7’s async/await
:
// The `exec` call wrapped in a promise
const getSecretValue = (attribute, value) => {
return new Promise((resolve, reject) => {
exec(
`secret-tool lookup ${attribute} ${value}`,
(error, stdout, stderr) => {
if (error) {
return reject(error)
}
if (stderr) {
return reject(stderr)
}
resolve(stdout.trim())
}
)
})
}
It’s a bit deep to get into promises and
async/await
here, but suffice to say they are patterns of writing asynchronous in a top-down or function-chaining way.
This function returns a promise which can then be used pretty easily:
getSecretValue('my-secret-app', 'token')
.then((secretToken) => {
// do something with it here
})
.catch((error) => {
// handle the error here
})
Now that we have a mechanism to safely handling my github personal access token, let’s get the information from GitHub!
Using Octokit
Github as several maintained packages to interact with its REST-api, and I just went with their @Octokit/rest
npm package to do a basic GET
request for the user’s information with token authentication.
const { Octokit } = require('@octokit/rest')
async function getCoauthorCommitTrailer(user) {
const octo = new Octokit({ auth: token })
const response = await octo.request(`/users/${user}`)
const { name, email } = response.data
return `Co-authored-by: ${name} <${email}>`
}
Neat! Super simple! But what if I want more than one user at once? Let’s amp it up with a higher order function!
async function getCoauthorCommitTrailers(...usernames) {
const responses = await Promise.all(
usernames.map((username) => oktokit.request(`/users/${username}`))
)
function toCommitTrailer(response) {
const { name, email } = response.data
return `Co-authored-by: ${name} <${email}>`
}
return responses.map(toCommitTrailer) // This returns the array in a promise
}
getCoauthorCommitTrailers(neenjaw, neenjaw_friend).then((trailers) =>
trailers.forEach((trailer) => console.log(trailer))
)
Now we can get a bunch at once!
Wrapping it up
Putting that all together, I wrapped it in a little CLI script using the yargs
npm package to parse command line options to set/unset/show the saved token. Updated my package.json file to include my global install script, then published it! Check out my npm package here: Give Credit Where Due
It was neat to put into practice things that I’ve seen done, but never implemented my self.
I really like that about programming – solving practical problems!