Version-Control Your Workflow, Not Just Your Code

Every firmware project has a set of commands you run constantly: build, flash, run tests, format code, etc. After a week on a project you know them by heart. After a month away, you’ve forgotten half of them.

VS Code tasks solve this cleanly. They live in a tasks.json file inside the project, get committed to the repository alongside the code, and are available to anyone who opens the project. No readme-hunting. No asking a colleague. Just run a keyboard shortcut and pick a task from the list.

How we define a task

Every task we write follows the same pattern: a label that appears in the picker, and a shell command that runs when you select it. Here’s a real example from one of our projects that builds firmware:

Rather than calling a locally installed compiler, the command runs inside one of our public Docker images. This means a developer can clone the project, open it in VS Code, and build without installing a compiler or any other tool. The -v flag mounts the project directory into the container, and -u $(id -u):$(id -g) ensures generated files are owned by the current user rather than root.

The result is a build environment that is identical across every machine on the team — no “works on my machine” surprises.

Fewer decisions at run time

Where tasks get particularly useful for embedded work is in handling the details that are easy to get wrong. Hardware revisions are a good example — rather than passing the wrong flag to a build command, we define the valid options directly in the task. VS Code presents them as a dropdown when the task runs, so the developer picks a revision and the build proceeds with the correct value. No flags to remember, no typos.

We also chain tasks together for sequences that always go in a fixed order. Getting firmware onto hardware means cleaning, building, and flashing — in that order, every time. Rather than leaving that sequence in a README or relying on someone knowing the right order, we encode it as a single task:

A complete project task list

To give a sense of scope, a typical project exposes tasks like these:

  • code formatting
  • code generation
  • code linting
  • unit testing
  • cleaning build environment
  • building firmware and bootloaders
  • flashing firmware onto devices
  • running applications, utility scripts, or tools
  • generating DFU packages and signing images
  • running acceptance tests on hardware
  • any combination of the above

The onboarding payoff

The real value shows up any time someone encounters the project fresh — a new developer on their first day, a teammate returning after months on another project, or an AI coding agent working through a change. In each case the task list acts as executable documentation: it says what operations exist and provides a reliable way to run them immediately, without reconstructing anything from scratch.

For an embedded firmware team in particular, where the gap between “editor open” and “code running on hardware” involves compilers, flashing tools, serial monitors, and Docker containers, that reduction in friction compounds quickly.