What are GitHub Workflows?
β’
I've written about GitHub Actions a couple of times (because they're rad), but I haven't talked much about workflows. They're a vital part of actually using Actions, so let's take a look at what they are, how to write them, and some π₯ tips I've picked up from lurking in the Actions team's Slack channel.
The heck is a workflow?
#Hopefully you're familiar with GitHub Actions; if not, check out this great article by @sarah_edo. Workflows are what define when and how a series of Actions should be run. You can think of it as the plan of Actions (pun 1000% intended).
GitHub will look for files with the .workflow extension in your repository's .github folder. Let's take a look at an example .github/main.workflow file:
workflow "Test my code" {
on = "push"
resolves = ["npm test"]
}
action "npm install" {
uses = "actions/npm@v1"
args = ["install"]
}
action "npm test" {
uses = "actions/npm@v1"
args = ["test"]
needs = ["npm install"]
}
In non-code, this workflow says:
When someone pushes, it will trigger the
npm testactionβwhich "needs" thenpm installaction.
GitHub doesn't just enact workflows in the repo's default branchβif you're working on a .workflow file on a branch, be aware that GitHub will run the workflow.
The workflow object
#workflow "Test my code" {
on = "push"
resolves = ["npm test"]
}
Here's where we define the workflow's metadata. This only takes two fields; on and resolves.
The on field is the webhook event name that will trigger your workflow. Note that not all webhook events you might be used to using are available to GitHub Actions; you'll want to double check in the docs.
The resolves field is the name of the action that should be run first. An important note is that actions can have "dependencies" by defining a needs property. GitHub will consider those needs properties before going to run your actions.
The action objects
#Each action object defines what action is run, what arguments you can pass, even what command is run inside of the action. Some key notes:
- You can have up to 100
actionobjects in a workflow - The
needsproperty will define a dependency tree. Actions will be run according to whatever actions it needs - Actions can write to directories that are shared with every subsequent action. Things like
npm install, which adds a wholenode_modulesdirectory, will persist across the actions in your workflow.
The individual fields here are a lot more complicated because there are different ways to use each one. Let's dig in!
uses
#Arguably the most important field, uses defines what action you want to run. This can be any of the following:
owner/repo@ref./<filepath>docker://image
The first option is pretty straightforward. It points to a repository on GitHub at the given ref. This can be a SHA, a tag or a branch; a common one might be JasonEtco/lights-camera-action@v1.0.0. A handy trick to know with this method is that it's actually owner/repo[/path]@ref; so you can point to a subdirectory of your repository.
This can be handy if you have one repo with a bunch of actions in it. However, I wouldn't recommend doing that. It's sort of like the whole "are monorepos good" debate, but GitHub has all kinds of discoverability hints for actions that are their own repos.
Now, you can also point your workflow to a folder in the same repository; so given a file tree like this:
βββ .github
β βββ main.workflow
βββ my-action
βββ Dockerfile
βββ entrypoint
You can point your workflow to the my-action directory. Your uses key must start with a ./, to specify that you want to use a file in this repo:
action "My action" {
uses = "./my-action"
}
One important note: any path must have a valid Dockerfile; otherwise, the action will fail.
Lastly, and this is a really awesome feature, you can just use an arbitrary Docker image. This is amazing, because if all you want to do is run a command you don't even need a whole action:
action "My action" {
uses = "docker://alpine"
run = "echo"
args = ["Hello", "World"]
}
Like with most things I talk about, you should check out the GitHub Action docs for more details!
args
#This property allows you to pass information to your workflow via command line arguments. So, given a workflow like this:
action "npm install" {
uses = "actions/npm@v1"
args = "install"
}
This will use the actions/npm action, whose entrypoint command runs npm $*. We're passing the argument install; so at the end of the day, it'll be npm install.
It's a fairly straightforward field, but let's take a look at some practical examples and how you might design an action that depends on user-set args.
My action JasonEtco/create-an-issue creates a new issue from a given template. By default, it will read from the .github/ISSUE_TEMPLATE.md fileβbut you can pass an argument to specify a different file:
action "Create issue" {
uses = "JasonEtco/create-an-issue@v2"
secrets = ["GITHUB_TOKEN"]
args = ".github/some-other-template.md"
}
Doing this makes the action way more extensible; you can have multiple workflows that use this functionality to open different issues.
Another thing I want to call out is that the args field is, by design, very barebones. In a Node.js action, for example, the arguments are passed as an array through process.argsv. That's totally standard, but it'd be amazing to give users an even more targeted way of configuring your action. You can use tools like actions-toolkit (which uses minimist under the hood) to parse arguments into something more declarative, by using --flags.
secrets
#Need to interact with a third party API? Want to make requests directly back to GitHub's API? Well, those things often require secret credentials to be passed. Fortunately, GitHub has a method for passing secrets to actions via the secrets field. These can be set in the GitHub UI, and are stored per-repo, which means that no other repository will be able to read those secrets.
Let's take a look at a practical example. Here's a workflow that compiles a TypeScript project, then publishes the compiled version to NPM:
workflow "Publish" {
on = "release"
resolves = ["npm publish"]
}
action "npm ci" {
uses = "actions/npm@v1"
args = "ci"
}
action "npm run build" {
needs = ["npm ci"]
uses = "actions/npm@v1"
args = "run build"
}
action "npm publish" {
needs = ["npm run build"]
uses = "actions/npm@v1"
args = "publish"
secrets = ["NPM_TOKEN"]
}
You'll see that we're passing an NPM_TOKEN to authenticate our publishing step. Without it, we wouldn't have permission to publish our library.
One handy thing to note is that the GITHUB_TOKEN is special. Its always set in your repository. You still have to decide if you want to pass it to secrets, but it'll let you make API requests and authenticate with GitHub.
There are some more fields you can pass, but these should be all you need for most workflows. Let me know what nifty workflows you build β¨