Caution
This project is still very much experimental. The CLI, template format, and generated workflow shape may change without notice. Pin a specific commit if you depend on it, and don't be surprised by breaking changes between versions until 1.0.
Generate GitHub Actions workflows from your Taskfile.yaml, so the commands you
run locally are the same ones CI runs — guaranteed, not by hand.
You write a workflow template under .task2ci/workflows/<name>.yaml —
plain GitHub Actions YAML with # @ci: <tag> placeholder comments where you
want task-driven steps spliced in. You mark the go-task tasks that should
fill those slots with the same # @ci: <tag> annotation in Taskfile.yaml.
Run task2ci and each template is rendered to .github/workflows/<name>.yaml
with the matching task steps in place. A task2ci -check mode fails CI if the
on-disk workflow has drifted from the template/Taskfile, so the two can't
quietly diverge.
Any project that uses go-task ends up with two parallel lists of commands:
Taskfile.yaml— what developers run locally (e.g.task test,task lint).github/workflows/ci.yaml— what CI runs (the same commands, restated by hand in YAML)
These drift. Someone adds a new check to the Taskfile but forgets the workflow, or vice versa, and "works on my machine" creeps in.
task2ci keeps the Taskfile as the source of truth for what runs, and
keeps the template as the source of truth for the rest of the workflow
(triggers, runner image, setup steps, conditionals, environments). The
template is plain GitHub Actions YAML, so you get full validation from
yaml-language-server, actionlint, and any other GHA tooling — task2ci itself
does not model setup steps, runners, or any other workflow structure.
The tool is language-agnostic; the Quick start below uses a Go example
because that's the dogfood, but the only Go-specific behavior is an
optional optimization that uses
go tool task when go-task is registered as a Go tool dependency.
In a project that already has a Taskfile.yaml:
# Install task2ci. Go projects can register it as a tool dep:
go get -tool arnested.dk/go/task2ci
# Anywhere else, install the binary with `go install` or download a
# release build, then make sure `task2ci` is on the runner's PATH.Create a template at .task2ci/workflows/ci.yaml. This is plain GitHub
Actions YAML; swap the setup steps for whatever your project needs
(setup-node, setup-python, system packages, …):
---
name: ci
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
# Set up the toolchain your tasks need. This Go example uses
# actions/setup-go; replace with what your project needs.
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Check generated CI is up to date
run: go tool task2ci -check
# @ci: testAnnotate the tasks you want spliced in:
# Taskfile.yaml
---
version: '3'
tasks:
# @ci: test
test:
desc: Run unit tests
cmd: go test ./...
# @ci: test
vet:
cmd: go vet ./...
# local-only — no annotation
tidy:
cmd: go mod tidyGenerate:
task2ci # or `go tool task2ci` if you registered it as a tool depYou get .github/workflows/ci.yaml, identical to the template except the
# @ci: test line is replaced by:
- name: Run unit tests
run: task test
- name: vet
run: task vetCommit Taskfile.yaml, the template, and the generated workflow. CI runs
task2ci -check (which you put in the template) on every push, so any
drift fails the build.
The same syntax works in two places:
-
In
Taskfile.yaml, on any task — opts that task into a tag:# @ci: <tag> # tag only # @ci: <tag> | <step name> # tag plus display-name override
-
In a template, in
.task2ci/workflows/<name>.{yaml,yml}— marks a splice point. Templates do not accept the| <step name>form; step naming is owned by the task side.
The step's name: in the generated workflow is chosen in this order:
- Override from the annotation (
| step name) inTaskfile.yaml - The task's
desc: - The task name itself
- Live under
.task2ci/workflows/<name>.yaml(or.yml— both accepted). - Are plain GitHub Actions workflow YAML; everything except
# @ci: <tag>placeholder comments is copied through verbatim. - Render to
.github/workflows/<name>.yaml(output is always.yaml). - Get a single autogenerated-header comment prepended to the output.
Anything that should run in CI other than the task-driven steps — setup actions, environment variables, conditionals, the drift-check step itself — goes directly into the template in GHA's native syntax.
Multiple template files produce multiple workflow files:
.task2ci/workflows/ci.yaml → .github/workflows/ci.yaml
.task2ci/workflows/release.yaml → .github/workflows/release.yaml
Each is independent.
- A
# @ci: <tag>annotation inTaskfile.yamlwith no matching template placeholder prints a warning and the task is left out of CI. - A
# @ci: <tag>placeholder in a template with no matching task prints a warning and the placeholder is removed from the generated output.
Both are warnings (stderr), not errors — generation still succeeds.
task2ci [flags]
- (no flags) — Render each template under
.task2ci/workflows/to.github/workflows/<name>.yaml. -check— Compare what would be generated now against the on-disk workflow files. Exit non-zero on any drift or orphan tag/placeholder. Used in CI.-fix— Remove orphan# @ci: <tag>placeholders (tags no task uses) from templates in place. Doesn't regenerate workflows; runtask2ciafter.-init— Write a minimal starter template at.task2ci/workflows/ci.yaml. Refuses to overwrite.-license— Print the license (MIT) and exit.-taskfile <path>— Path to a Taskfile. May be repeated to scan multiple files. Default: auto-discover (Taskfile.yml→taskfile.yml→Taskfile.yaml→taskfile.yaml→.distvariants).
-check, -fix, and -init are mutually exclusive.
The default behavior is language-agnostic: inserted steps run
task <task-name>, and the user's template installs task however it
wants (the go-task/setup-task action is the typical choice).
The one Go-specific optimization: if task2ci finds a go.mod in the
working directory that registers github.com/go-task/task/v3/cmd/task as
a tool directive (Go 1.24+ tool dependencies), the generated run: lines
use go tool task <name> instead. CI then uses the exact go-task version
pinned in your go.mod — no separate install step needed (though you
still need actions/setup-go in your template).
Non-Go projects: just include
uses: go-task/setup-task@v2 in the template; the run: task <name>
lines work the same way.
If you use AI coding assistants (Claude Code, Cursor, Copilot, etc.) in
your project, paste the snippet below into your AGENTS.md /
CLAUDE.md / .cursorrules / whatever project-rules file your tool
reads. It keeps them from trying to hand-edit the generated workflow
when they should be editing the template instead.
This project uses task2ci to generate GitHub Actions workflows.
- Source of truth:
- `.task2ci/workflows/<name>.yaml` — workflow templates.
- `Taskfile.yaml` — task definitions, opted into CI via
`# @ci: <tag>` annotations.
- Generated, do not hand-edit:
- `.github/workflows/<name>.yaml` — regenerated from the templates
on every `task2ci` run.
- After changing a template or annotation, run `task2ci` to regenerate
the workflow files and commit the result.
- CI runs `task2ci -check` and fails on drift, orphan tag
annotations, or orphan placeholders.
- Use `task2ci -fix` to delete orphan placeholders from templates.
- Use `task2ci -init` to scaffold a starter template.
Full docs: https://arnested.github.io/task2ci/