Skip to content

Use Task in GitHub Actions with tenthirtyam/setup-task

If you have been using Task (go-task/task) as your task runner and build tool, you have probably run into the same friction I did: getting it installed cleanly inside a GitHub Actions workflow is more annoying than it should be. There is no native support on GitHub-hosted runners, so you end up writing a curl one-liner, hand-rolling a cache step, or copy-pasting boilerplate across every workflow in every repository. It works, but it is not great.

tenthirtyam/setup-task is my solution to that problem. It is a purpose-built GitHub Action that handles downloading, caching, and configuring Task in a single step so you can get back to what actually matters: the tasks themselves.

Why Task Instead of Make?

If you have not tried Task yet, here is the quick pitch.

make and Makefile have been around forever, and they work. But Makefile syntax has a lot of sharp edges: tabs vs. spaces, platform-specific shell assumptions, .PHONY declarations that are easy to forget, and a mental model that was designed around file targets rather than task orchestration. Once you need to do something slightly beyond the basics, things get awkward fast.

Windows makes this noticeably worse. make is not installed by default, so contributors end up choosing between Git Bash, Chocolatey, MSYS2, and WSL, then spend time figuring out why their Makefile works on one and not the others. Task ships as a single Go binary with no shell assumptions and no extra dependencies. Every task runs identically on Windows, macOS, and Linux.

Task takes a different approach. It uses a clean Taskfile.yml written in YAML to define tasks, accepts variables from the CLI and environment, supports dependency ordering between tasks, generates tab completion, and ships as a single self-contained Go binary. It is readable, cross-platform, and easy to onboard contributors to. If you have ever stared at a cryptic Makefile and wondered why CI runs fine but local runs do not, you will appreciate how much nicer a Taskfile.yml is to work with.

I have adopted Task across many open-source projects and it has been a consistent quality-of-life improvement. However, there is one catch: unlike make, Task is not pre-installed on GitHub-hosted runners. That gap is exactly what setup-task is designed to close, with a few options that make it genuinely useful beyond a simple download script.

Basic Usage

The simplest usage installs the latest release of Task:

- name: Setup Task
  uses: tenthirtyam/setup-task@v1
  with:
    version: 'latest'
    github-token: ${{ secrets.GITHUB_TOKEN }}

Providing github-token is strongly recommended. The action calls the GitHub API to resolve the latest release and, without a token, unauthenticated requests can hit rate limits in busy CI environments.

After this step runs, the task binary is on PATH for every subsequent step in the job.

Pinning to a Specific Version

Pinning keeps your CI deterministic. Pass an exact version string with or without the leading v:

- name: Setup Task
  uses: tenthirtyam/setup-task@v1
  with:
    version: '3.43.1'
    github-token: ${{ secrets.GITHUB_TOKEN }}

Version from File

For repositories that want a single source of truth for the Task version, the version-from-file input reads the version from a file in the repository (e.g., .task-version):

- name: Setup Task
  uses: tenthirtyam/setup-task@v1
  with:
    version-from-file: '.task-version'
    github-token: ${{ secrets.GITHUB_TOKEN }}

The file should contain a single line with the version string, such as 3.43.1. This pairs well with Renovate or Dependabot to automate Task version bumps in a predictable, reviewable way.

Note

version and version-from-file are mutually exclusive. Specifying both will cause the action to fail with an error.

Caching

The action caches the Task binary by default using the standard @actions/cache toolkit. On subsequent runs with the same version, the binary is restored from cache rather than re-downloaded, which keeps workflow run times fast.

If you need to bypass the cache for any reason, set skip-cache: 'true':

- name: Setup Task
  uses: tenthirtyam/setup-task@v1
  with:
    version: 'latest'
    skip-cache: 'true'
    github-token: ${{ secrets.GITHUB_TOKEN }}

Passing Variables

The vars input forwards CLI variables directly to Task, which is useful when your Taskfile.yml accepts configuration at run time:

- name: Setup Task
  uses: tenthirtyam/setup-task@v1
  with:
    version: 'latest'
    github-token: ${{ secrets.GITHUB_TOKEN }}
    vars: |
      ENV=production
      VERSION=${{ github.ref_name }}

These variables are available to any task that runs after the setup step.

Output

The action exposes a task-path output containing the absolute path to the installed task binary. This is useful if you need to reference it explicitly in a subsequent step:

- name: Setup Task
  id: setup-task
  uses: tenthirtyam/setup-task@v1
  with:
    version: 'latest'
    github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Print Task Path
  run: echo "Task installed at ${{ steps.setup-task.outputs.task-path }}"

Pinning to a Commit Hash

For production workflows where supply-chain security matters, pin to a specific commit hash rather than a mutable tag:

- name: Setup Task
  uses: tenthirtyam/setup-task@7969e9328f7c8a5ea77d2c7aea9cfdc5c965c13e # v1.0.2
  with:
    version: 'latest'
    github-token: ${{ secrets.GITHUB_TOKEN }}

The comment preserves human-readable context while the hash guarantees you are running exactly the code you reviewed.

Putting It All Together

The real payoff becomes clear when you see how setup-task fits into an actual project. Imagine a Go project with a Taskfile.yml like this:

# Taskfile.yml
version: "3"

tasks:
  build:
    desc: Build the project.
    cmds:
      - go build -v ./...

  test:
    desc: Run all tests with verbose output.
    cmds:
      - go test -v ./...

  test-race:
    desc: Run all tests with race condition detection.
    cmds:
      - go test -v -race ./...

  lint:
    desc: Run golangci-lint to check code quality.
    cmds:
      - golangci-lint run

  fmt:
    desc: Format all Go source code.
    cmds:
      - go fmt ./...

  vet:
    desc: Run go vet to examine Go source code.
    cmds:
      - go vet ./...

  check:
    desc: Run all code quality checks.
    deps: [fmt, vet, lint, test]

  ci:
    desc: Run the full CI pipeline.
    deps: [lint, test-race]

Now here is what makes this worth it. Every developer on the project runs task test or task check locally. And in CI, the GitHub Actions workflow calls the exact same thing:

name: Test

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - name: Setup Go
        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
        with:
          go-version-file: go.mod
      - name: Setup Task
        uses: tenthirtyam/setup-task@7969e9328f7c8a5ea77d2c7aea9cfdc5c965c13e # v1.0.2
        with:
          version: 'latest'
          github-token: ${{ secrets.GITHUB_TOKEN }}
      - name: Build
        run: task build

  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - name: Setup Go
        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
        with:
          go-version-file: go.mod
      - name: Setup Task
        uses: tenthirtyam/setup-task@7969e9328f7c8a5ea77d2c7aea9cfdc5c965c13e # v1.0.2
        with:
          version: 'latest'
          github-token: ${{ secrets.GITHUB_TOKEN }}
      - name: Run Tests
        run: task test
      - name: Run Tests with Race Detection
        run: task test-race

  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - name: Setup Go
        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
        with:
          go-version-file: go.mod
      - name: Setup Task
        uses: tenthirtyam/setup-task@7969e9328f7c8a5ea77d2c7aea9cfdc5c965c13e # v1.0.2
        with:
          version: 'latest'
          github-token: ${{ secrets.GITHUB_TOKEN }}
      - name: Run Linters
        uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
        with:
          version: 'latest'

task build, task test, task test-race in the workflow call exactly the same commands you would run locally. No more "it passed on my machine" moments. The Taskfile.yml is the contract between local and CI, and setup-task is what makes CI honor it.

The same pattern works just as well for TypeScript projects. Swap in your Node.js setup step and your tasks become things like task lint (ESLint), task test (Vitest or Jest), and task build (tsc or your bundler of choice). The Taskfile.yml structure and the workflow wiring stay the same regardless of the language.

Inputs Summary

Input Description Default
version Task version to install (latest, 3.43.1, v3.43.1, etc.). latest
version-from-file Path to a file containing the Task version. Mutually exclusive with version. ''
skip-cache Set to 'true' to bypass the action cache. 'false'
github-token GitHub token for API authentication. Recommended. ''
vars Custom variables for the Task CLI as a multi-line key=value list. '{}'
verbose Set to 'true' to enable detailed debug logging. 'false'

Getting Started

The action is published to the GitHub Marketplace at tenthirtyam/setup-task and the source is available on GitHub at github.com/tenthirtyam/setup-task.