Package GitHub Actions automatically with GitHub Actions

Samuel Ryan
prompt
Published in
4 min readJun 15, 2021

Actions are individual tasks that you can combine to create jobs and customize your workflow. You can create your own actions, or use and customize actions shared by the GitHub community.

A GitHub Action shared by the GitHub community is distributed through a GitHub repository.

GitHub downloads each action run in a workflow during runtime⌝ and executes it as a complete package of code [that] must include any package dependencies required to run the JavaScript code¹

The typical process to meet this requirement is a dist directory that each commit author is responsible for keeping up to date with their changes — as seen in first-party actions⌝ and third-party actions⌝ — a fragile, manual process.

Fortunately, we can automate the process of packaging GitHub Actions for distribution using GitHub Actions itself — following a few simple steps. Read on to learn how and why, or skip straight to the complete example.

Why automate the process of packaging GitHub Actions?

Ease of Contribution

The barrier to entry for contributing to a GitHub project should be as low as possible; this means enabling contributions created on the GitHub website — without access to build tooling.

Vulnerability to Malicious Changes

Packaged code is difficult to review; it obscures changes by transpiling, compiling and minifying, while Code Review interfaces often hide differences in generated files.

How can we use GitHub and GitHub Actions to build a robust packaging process?

  • A main branch protected from pushes⌝— changes come from Pull Requests
  • The main branch does not contain any packaged code, the dist directory in .gitignore prevents any unintentional push of packaged code
  • A single release branch off main which a Workflow keeps synchronised by merging new changes from main into release
  • Any push to therelease branch triggers the Workflow, this Workflow automatically packages the action and pushes the resulting dist onto the release branch
  • A Workflow prevents tags from being added to commits that do not contain a dist (i.e., not on the release branch)

1. Create a Release Branch

You can create branches directly on GitHub, as described in Creating and deleting branches within your repository⌝ — create a branch called release from the main branch. We need to keep release up to date with any changes merged into main which we can do from a Workflow.

Pushing new commits from a Workflow

GitHub executes each Workflow with a token — ${{ github.token }} — that has permissions to push commits back to a repository; however, to prevent infinite Workflow loops, the tasks performed with this token do not trigger further Workflow runs.

GitHub recommends a Personal Access Token for workflows that need the ability to trigger further workflows. Unfortunately, Personal Access Tokens cannot be scoped to a repository, violating the principle of least privilege. Fortunately, there is an alternative: a Deploy Key.

A complete step by step guide to using a Deploy Key instead of a Personal Access Token is available on this blog, in Trigger another GitHub Workflow — without using a Personal Access Token.

2. Package Action on Push

Synchronise release with main

A push to the main branch triggers our Workflow. The job checks out the release branch using the commit key and then merges main into the job workspace.

Package changes

All dependencies are installed, the action is packaged.

Push Packaged Action

The source changes and dist changes are pushed to the release branch.

3. Run integration tests against the package

Tests run against every commit, with both unit tests and integration tests. The additional step required is determining if the action should be packaged or not: on unpackaged branches, we need to package; on packaged branches, we want to use dist as is — this is important as it tests that our packaged releases work.

View the complete test.yml Workflow in pr-mpt/examples-action-packaging⌝ on GitHub⌝

4. Protect commits without the package from being tagged

An unpackaged Action will cause the Workflow to fail with an unhelpful error that Action consumers may not understand.

Error: File not found: '/home/runner/work/_actions/organisation/repository/main/dist/index.js'

Avoiding this error is straight-forward: do not create tags against commits that aren’t on the release branch, but human error is inevitable, so we can implement a Workflow to protect from this mistake.

💡 GitHub warns that “you will not receive a webhook for this event when you push more than three tags at once⌝”, which means there is an edge-case that this Workflow cannot catch, and so some diligence is required.

5. Limitations and caveats

Branch references are unsupported

GitHub recommends using tags for release management⌝and only list tags in the Marketplace, which this solution supports — along with commit pinning. However, a user may expect that they can continue to reference an action by branch— e.g.: pr-mpt/examples-action-packaging@main — which is not possible because main is no longer packaged.

Release a patch

The main and release branching strategy requires an additional branch for any change to a previous major or minor version.

  1. Locate the commit on main which represents the source for the major or minor version
  2. Create a new fix branch using the main commit as the base (e.g: git checkout -b fix-a-bug 3ad5247)
  3. Push your changes to the new branch
  4. Create a new release branch from the fix branch (e.g: git checkout -b release/fix-a-bug)
  5. After the package-action job is complete, tag the packaged Action commit with the version

Automatic packaging in action

A complete example is available on GitHub in pr-mpt/examples-action-packaging⌝. Fork it, make changes and see automatic packaging in action.

For a real-world example, many of the actions in the pr-mpt organisation on GitHub⌝ use this approach, including:

--

--