Packaging an Action
After having created several actions to use in GitHub Actions workflows, I've settled on a pattern that I really like for packaging them, and in my most recent action, I codified this in a CI process to automate it for me.
The reason that this is important is because GitHub Actions uses a git repository as the distribution mechanism for an action. When you specify an action to run, like:
steps:
- uses: actions/checkout@v2
That indicates that you want to run an action named actions/checkout
at version v2
. This literally maps to a reference v2
in the repository https://github.com/actions/checkout
.
Since actions are actually just Node.js applications, that means that the reference v2
in that repository needs to contain the application, and its dependencies. It needs to actually contain the node_modules
directory. But... you're not supposed to check that in, are you?
No! You're not. At least... not in your development branch. Now, of course you could technically do this and GitHub Actions would work just fine, but it's going to be messy. Instead, I recommend using a two branch approach:
-
master
is the branch that you work in, just like you would with any application. In this branch, you should not check in yournode_modules
directory, it should be added to your.gitignore
just like any other Node.js application. -
dist
is the branch that your application is distributed in. This contains the built and packed version of your action, along with any metadata files (your license, README andaction.yml
, for example).
This keeps the build output separate from your source directory, where it definitely doesn't belong. But it's a little annoying to have to build into a new branch and publish it yourself. And - whenever I see anything that's a manual annoyance, I try to automate it. So I created a GitHub Actions workflow to build my master
branch and then publish it into dist
.
Here's the simple summary (with comments to explain what's happening):
name: CI
# Run this whenever there's an update to the master branch.
on:
push:
branches: [ master ]
jobs:
#
# Build (if there's a build step) and run tests to ensure that the
# new change in master is good.
#
build:
runs-on: ubuntu-latest
steps:
- name: Check out source
uses: actions/checkout@v2
- name: Build and Test
run: |
npm ci
npm run build --if-present
npm test
#
# Publish the action to the `dist` branch
#
publish_action:
runs-on: ubuntu-latest
needs: build
steps:
- name: Check out source
uses: actions/checkout@v2
# Check out the `dist` branch into the `dist` directory.
- name: Check out distribution branch
uses: actions/checkout@v2
with:
ref: 'dist'
path: 'dist'
# Run `npm run pack`, which uses @zeit/ncc to package the action
# into a single file. Copy things that we want to publish out of
# the source directory and into the dist directory (which is where
# the dist branch is checked out.)
- name: Package
run: |
npm install
npm run pack
mkdir -p dist/documentation
mkdir -p dist/examples
cp action.yml dist/
cp README.md dist/
cp LICENSE.txt dist/
cp documentation/* dist/documentation/
cp examples/* dist/examples/
# Check for changes; this avoids publishing a new change to the
# dist branch when we made a change to (for example) a unit test.
# If there were changes made in the publish step above, then this
# will set the variable `has_changes` to `1` for subsequent steps.
- name: Check for changes
id: status
run: |
source ../.github/workflows/actions.sh
if [ -n "$(git status --porcelain)" ]; then
echo "::set-output name=has_changes::1"
fi
working-directory: dist
# Commit the changes to the dist branch and push the changes up to
# GitHub. (Replace the name and email address with your own.)
# This step only runs if the previous step set `has_changes` to `1`.
- name: Publish action
run: |
git add --verbose .
git config user.name 'CI User'
git config user.email 'ci@example.com'
git commit -m 'Update from CI'
git push origin dist
if: steps.status.outputs.has_changes == '1'
working-directory: dist
This workflow will keep dist
updated any time you make changes in the master
branch. To reference your action, you can run your/repo@dist
in a workflow, or better still, you can create a release off the dist branch. You can even publish your action to the marketplace from those releases.
Before you run this workflow, you'll need to create some commit in the dist
branch in your GitHub repository.
You can create an empty one by:
commit_id=$(git commit-tree -m 'Distribution branch' 4b825dc642cb6eb9a060e54bf8d69288fbee4904)
git push origin ${commit_id}:refs/heads/dist
(What's going on here? That's another blog post.)
I think that this workflow is a great way to keep a build of your action up-to-date, but also while keeping it out of your source branch.