GitHub Actions: Best Practices


This working document serves as a collection of best practices for making use of GitHub Actions. If you have any suggestions or additions, please open a pull request on GitHub!

Collection of Best Practices

Set timeouts for workflows

By default, GitHub Actions kills workflows after 6 hours if they have not finished by then. Many workflows don't need nearly as much time to finish, but sometimes unexpected errors occur or a job hangs until the workflow run is killed 6 hours after starting it. Therefore it's recommended to specify a shorter timeout.

The ideal timeout depends on the individual workflow but 30 minutes is typically more than enough for the workflows used in Exercism repos.

This has the following advantages:

  • PRs won't be pending CI for half the day, issues can be caught early or workflow runs can be restarted.
  • The number of overall parallel builds is limited, hanging jobs will not cause issues for other PRs if they are cancelled early.

Example

jobs:
  configlet:
    timeout-minutes: 30
    runs-on: ubuntu-latest
    steps:
      - [...]

Consider if (third-party) actions are really needed

Actions should be treated like dependencies in your favourite programming language1, they are code written by third party authors outside of the control of Exercism. Even if you trust the authors of the action, there may be a hostile takeover of the repository which will indirectly give those people access to Exercism repos, including write access.

Therefore, you should carefully consider if introducing a new action is really worth it or if it's better to move the code into a (new) action under Exercism's control.

Also consider if the action is actively maintained, e.g. by checking recent repo activity or ensuring that the action is part of an organisation rather than an individual account.

Actions published by GitHub or the Exercism org can generally be considered as safe(ish) to include without special consideration.

Limit scope of workflow token

By default, the access token given to the workflow has wide-ranging permissions, both for reading and writing.

The principle of least privilege should also be applied to workflows.

You can specify which permissions a workflow needs on a per-workflow basis.

Example

If a workflow only needs to read the contents of a repo but not write to it, e.g. because it's a normal CI check, you can restrict the token as follows:

permissions:
  contents: read

Check the GitHub Docs for the full list of permissions.

Pin actions to SHAs

When using other actions, pin them to a commit (via their SHA), not to a branch or tag. This ensures that the same code will be executed each time, which is not guaranteed when pinning to a branch or tag.

This has two benefits:

  1. It makes your build stable
  2. It prevents an attacker from changing a branch/tag to point to malicious code

The only exception to this rule could be actions that we (Exercism) have built ourselves.

Finding the commit SHA

Usually, you want to pin to the commit SHA of a specific release. To find a release's commit SHA, go to the action's repository releases page (e.g. https://github.com/actions/checkout/releases). Find the release you want to use and click on the shorthand SHA (e.g. a12a394) listed in the summary section to the left of the release. You'll then be redirected to the release details page, which lists the full commit SHA you can use.

Example

- name: Checkout code
  uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846

Pin test runners to version

Most workflows will run on GitHub's supported runners. When using one of these runners, use a specific version instead of the latest version.

This ensures that the workflow will always run on the same runner, which makes your build stable.

Example

Use:

runs-on: ubuntu-22.04

instead of:

runs-on: ubuntu-latest

Consider setting up a concurrency strategy

It's often not necessary or useful to run CI on intermediate commits if a newer commit has been pushed in the meantime.

You can configure a concurrency strategy in order to automatically cancel running workflows in the same context.

Example

To cancel intermediate builds in a PR, you can use the following concurrency settings:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}
Third Party Notice

The example above is based on PkgTemplates.jl's CI workflow, published under the MIT license:

MIT License

Copyright (c) 2017-2020 Chris de Graaf, Invenia Technical Computing Corporation

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Consider which triggers are really needed

Read the "Security hardening for GitHub Actions" guide

The practices mentioned above are by no means exhaustive. For a comprehensive guide on good security practices for using GitHub Actions safely, check out GitHub's security guide.

Workflow Checklist

You can use the following checklist to check if a workflow follows the best practices. The checklist is not meant to be complete but instead focuses on the most important items.

Copy-pastable version, e.g. for PRs

  1. unless the language uses the npm ecosystem. ↩