/setup-action
Sets up GitHub Actions for ESP32 projects — building ESP-IDF firmware in a pinned container, taking the version from the git tag, proving a published image is the variant it claims to be, and publishing artefacts and a release when a tag is pushed. Also covers the pre-merge
$ npx -y skills add SensorsIot/Embedded-AI-Harness --skill setup-action --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/setup-action
Context preview
The summary Claude sees to decide when to auto-load this skill.
Sets up GitHub Actions for ESP32 projects — building ESP-IDF firmware in a pinned container, taking the version from the git tag, proving a published image is the variant it claims to be, and publishing artefacts and a release when a tag is pushed. Also covers the pre-merge
SKILL.md
setup-action.SKILL.mdname: setup-action
description: Sets up GitHub Actions for ESP32 projects — building ESP-IDF firmware in a pinned container, taking the version from the git tag, proving a published image is the variant it claims to be, and publishing artefacts and a release when a tag is pushed. Also covers the pre-merge gate: lint plus the tests a hosted runner can actually run, and which tiers it cannot reach. Use this skill whenever CI comes up for an ESP32 or testbench project — a new project needing a build workflow, a release that should publish firmware, a gate that keeps turning red, or the question "why isn't this checked automatically". Triggers on "GitHub Action", "CI", "workflow", "pipeline", "build firmware in CI", "publish a release", "release on tag", ".github/workflows", "pre-merge check", "self-hosted runner", "lint on push".
CI for ESP32 projects
Two workflows with different jobs, and most projects want both:
| Workflow | Trigger | Answers | |---|---|---| | **build** | every push; publishes on a version tag | Can this be flashed, and where is the artefact? | | **gate** | every push and PR | May this change land? |
The build workflow is the substantial one and comes first below. The gate is simpler but has a rule that decides whether it is worth having at all: **a gate must be green from its first run**, or it teaches everyone to ignore a red tick.
Neither replaces hardware testing. A green tick means it compiles and the pure logic passes.
---
Part 1 — Build and publish firmware
Put this at `.github/workflows/build.yml` and fill in `<firmware-dir>`, `<app-name>` and `<target>`. Drop `working-directory` if the ESP-IDF project sits at the repository root.
**One build per run.** Most projects have one firmware, and the template defaults to that: `MULTI_VARIANT: 'false'` pins every run to production, and `<marker>` and `<alt-variant>` are then never read. Flip it to `'true'` only for a project that genuinely ships a second image, and read "Variants" below before you do.
name: Build Firmware
env:
IDF_TAG: v6.0.2 # keep in step with the container tag below
# The one switch for variant handling. Leave 'false' unless this project
# really builds a second image; then set <marker> too.
MULTI_VARIANT: 'false'
VARIANT_MARKER: <marker> # a string only the non-production image contains
on:
push:
branches: [main]
tags: ['v*.*.*']
workflow_dispatch:
inputs:
version:
description: 'Version number (e.g. 1.2.0)'
required: true
variant: # inert while MULTI_VARIANT is 'false'
description: 'Which firmware to build'
type: choice
options: [production, <alt-variant>]
default: production
permissions:
contents: write # required to create the release
jobs:
build:
runs-on: ubuntu-latest
container: espressif/idf:v6.0.2
defaults:
run:
working-directory: <firmware-dir>
shell: bash
steps:
# Must come first, and must not be skipped. The container runs as root
# while the workspace belongs to the runner user, so every git call in a
# `run:` step dies with "detected dubious ownership" — see "The container
# runs as the wrong user" below.
- name: Trust the workspace
working-directory: ${{ github.workspace }}
run: git config --global --add safe.directory '*'
- uses: actions/checkout@v4
with:
fetch-depth: 0 # see "Versioning" below — tags are needed
- name: Show the IDF version actually used
run: . $IDF_PATH/export.sh >/dev/null && idf.py --version
- name: Decide what to build
id: plan
run: |
VARIANT="${{ github.event.inputs.variant || 'production' }}"
[ "$MULTI_VARIANT" = "true" ] || VARIANT=production
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ github.event.inputs.version }}"
elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then
VERSION=${GITHUB_REF#refs/tags/v}
else
VERSION="0.0.0-$(git rev-parse --short HEAD)"
fi
DEFAULTS="sdkconfig.defaults"
if [ "$VARIANT" != "production" ]; then
DEFAULTS="sdkconfig.defaults;sdkconfig.$VARIANT.defaults"
VERSION="$VERSION-$VARIANT"
fi
{ echo "variant=$VARIANT"; echo "version=$VERSION"
echo "defaults=$DEFAULTS"; } >> $GITHUB_OUTPUT
echo "Building the $VARIANT firmware as $VERSION"
# -DSDKCONFIG: keep the config in the build dir — see "sdkconfig" below.
- name: Build firmware
run: |
. $IDF_PATH/export.sh >/dev/null
ARGS="-B build -DSDKCONFIG=$PWD/build/sdkconfig \
-DSDKCONFIG_DEFAULTS=${{ steps.plan.outputs.defaults }} \
-DPROJECT_VER=${{ steps.plan.outputs.version }}"
idf.py $ARGS set-target <target>
idf.py $ARGS build
- name: Verify the image matches the variant asked for
if: env.MULTI_VARIANT == 'true'
run: |
grep -q "$VARIANT_MARKER" build/<app-name>.bin && FOUND=yes || FOUND=no
WANT=no; [ "${{ steps.plan.outputs.variant }}" = "production" ] || WANT=yes
if [ "$FOUND" != "$WANT" ]; then
echo "FATAL: ${{ steps.plan.outputs.variant }} image, marker found=$FOUND, expected=$WANT"
exit 1
fi
echo "verified: ${{ steps.plan.outputs.variant }} image, marker $FOUND"
- name: Collect artefacts
run: |
V="${{ steps.plan.outputs.version }}"
cp build/<app-name>.bin firmware_v${V}.bin
cp build/<app-name>.elf firmware_v${V}.elf
# Cold-flash set, taken from flash_args rather than named here —
# see "Never hand-write the image list" below.
mkdir -p coldflash
cp build/flash_args coldflash/
awk '$1 ~ /^0x/ {printRead more
name: setup-action description: Sets up GitHub Actions for ESP32 projects — building ESP-IDF firmware in a pinned container, taking the version from the git tag, proving a published image is the variant it claims to be, and publishing artefacts and a release when a tag is pushed. Also covers the pre-merge gate: lint plus the tests a hosted runner can actually run, and which tiers it cannot reach. Use this skill whenever CI comes up for an ESP32 or testbench project — a new project needing a build workflow, a release that should publish firmware, a gate that keeps turning red, or the question "why isn't this checked automatically". Triggers on "GitHub Action", "CI", "workflow", "pipeline", "build firmware in CI", "publish a release", "release on tag", ".github/workflows", "pre-merge check", "self-hosted runner", "lint on push".
CI for ESP32 projects
Two workflows with different jobs, and most projects want both:
| Workflow | Trigger | Answers | |---|---|---| | **build** | every push; publishes on a version tag | Can this be flashed, and where is the artefact? | | **gate** | every push and PR | May this change land? |
The build workflow is the substantial one and comes first below. The gate is simpler but has a rule that decides whether it is worth having at all: **a gate must be green from its first run**, or it teaches everyone to ignore a red tick.
Neither replaces hardware testing. A green tick means it compiles and the pure logic passes.
---
Part 1 — Build and publish firmware
Put this at `.github/workflows/build.yml` and fill in `<firmware-dir>`, `<app-name>` and `<target>`. Drop `working-directory` if the ESP-IDF project sits at the repository root.
**One build per run.** Most projects have one firmware, and the template defaults to that: `MULTI_VARIANT: 'false'` pins every run to production, and `<marker>` and `<alt-variant>` are then never read. Flip it to `'true'` only for a project that genuinely ships a second image, and read "Variants" below before you do.
name: Build Firmware
env:
IDF_TAG: v6.0.2 # keep in step with the container tag below
# The one switch for variant handling. Leave 'false' unless this project
# really builds a second image; then set <marker> too.
MULTI_VARIANT: 'false'
VARIANT_MARKER: <marker> # a string only the non-production image contains
on:
push:
branches: [main]
tags: ['v*.*.*']
workflow_dispatch:
inputs:
version:
description: 'Version number (e.g. 1.2.0)'
required: true
variant: # inert while MULTI_VARIANT is 'false'
description: 'Which firmware to build'
type: choice
options: [production, <alt-variant>]
default: production
permissions:
contents: write # required to create the release
jobs:
build:
runs-on: ubuntu-latest
container: espressif/idf:v6.0.2
defaults:
run:
working-directory: <firmware-dir>
shell: bash
steps:
# Must come first, and must not be skipped. The container runs as root
# while the workspace belongs to the runner user, so every git call in a
# `run:` step dies with "detected dubious ownership" — see "The container
# runs as the wrong user" below.
- name: Trust the workspace
working-directory: ${{ github.workspace }}
run: git config --global --add safe.directory '*'
- uses: actions/checkout@v4
with:
fetch-depth: 0 # see "Versioning" below — tags are needed
- name: Show the IDF version actually used
run: . $IDF_PATH/export.sh >/dev/null && idf.py --version
- name: Decide what to build
id: plan
run: |
VARIANT="${{ github.event.inputs.variant || 'production' }}"
[ "$MULTI_VARIANT" = "true" ] || VARIANT=production
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ github.event.inputs.version }}"
elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then
VERSION=${GITHUB_REF#refs/tags/v}
else
VERSION="0.0.0-$(git rev-parse --short HEAD)"
fi
DEFAULTS="sdkconfig.defaults"
if [ "$VARIANT" != "production" ]; then
DEFAULTS="sdkconfig.defaults;sdkconfig.$VARIANT.defaults"
VERSION="$VERSION-$VARIANT"
fi
{ echo "variant=$VARIANT"; echo "version=$VERSION"
echo "defaults=$DEFAULTS"; } >> $GITHUB_OUTPUT
echo "Building the $VARIANT firmware as $VERSION"
# -DSDKCONFIG: keep the config in the build dir — see "sdkconfig" below.
- name: Build firmware
run: |
. $IDF_PATH/export.sh >/dev/null
ARGS="-B build -DSDKCONFIG=$PWD/build/sdkconfig \
-DSDKCONFIG_DEFAULTS=${{ steps.plan.outputs.defaults }} \
-DPROJECT_VER=${{ steps.plan.outputs.version }}"
idf.py $ARGS set-target <target>
idf.py $ARGS build
- name: Verify the image matches the variant asked for
if: env.MULTI_VARIANT == 'true'
run: |
grep -q "$VARIANT_MARKER" build/<app-name>.bin && FOUND=yes || FOUND=no
WANT=no; [ "${{ steps.plan.outputs.variant }}" = "production" ] || WANT=yes
if [ "$FOUND" != "$WANT" ]; then
echo "FATAL: ${{ steps.plan.outputs.variant }} image, marker found=$FOUND, expected=$WANT"
exit 1
fi
echo "verified: ${{ steps.plan.outputs.variant }} image, marker $FOUND"
- name: Collect artefacts
run: |
V="${{ steps.plan.outputs.version }}"
cp build/<app-name>.bin firmware_v${V}.bin
cp build/<app-name>.elf firmware_v${V}.elf
# Cold-flash set, taken from flash_args rather than named here —
# see "Never hand-write the image list" below.
mkdir -p coldflash
cp build/flash_args coldflash/
awk '$1 ~ /^0x/ {printOther skills on embedded-ai-harness.
- /build
Phase 3 of AI Closed-Loop Programming — the Build phase, and the driver of the whole loop: locate the project on the chain, name the next act, design and declare tests, dispatch code/flash/verify, correct until the tests run clean. Owns the test plan, test design, audit,
Open skill - /commission
Phase 2 of AI Closed-Loop Programming — Commissioning: prove the project's OWN never-seen-working parts (its board, its wiring, its peers/simulators), so that a failing test means the code and not the setup. The workbench itself is never commissioned by a project — its quality
Open skill - /define
Phase 0 of AI Closed-Loop Programming — Definition: engineers the WHAT the loop converges on. Writes and evolves the FSD — atomic, falsifiable, provenance-tagged requirements each carrying its verification contract — plus architecture, data model, interface definitions, state
Open skill - /esp-idf-handling
Complete ESP-IDF lifecycle: project setup, build, flash, monitor, and OTA. Automatically detects whether a workbench is available or the device is connected locally via USB. Covers sdkconfig, partition tables, esptool, RFC2217 remote flashing, GPIO download mode, OTA updates,
Open skill - /esp-pio-handling
PlatformIO lifecycle for ESP32 firmware: platformio.ini, environment selection, build, upload and serial monitor, on local USB or through the workbench. Covers what differs from ESP-IDF — the .pio/build layout, the boot_app0 image an Arduino-framework build needs, and RFC2217
Open skill - /grill-me
Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me".
Open skill

