GitHub Actions¶
The workflow below triggers on tag pushes matching v*, builds the
firmware, then uploads the binary + manifest to SimpleOTA. The build
step is yours (PlatformIO, idf.py, Arduino-CLI, or whatever you use); the
upload step is what SimpleOTA needs.
Required secrets¶
Add these in Settings → Secrets and variables → Actions of the firmware repo (not the SimpleOTA repo):
| Secret | Value |
|---|---|
SIMPLEOTA_BASE_URL |
e.g. https://simpleota.com |
SIMPLEOTA_PROJECT_ID |
UUID of the project (visible in the dashboard URL). |
SIMPLEOTA_TOKEN |
Project API token. Generate one in Project → Tokens. |
Minimal pipeline¶
name: build-and-upload
on:
push:
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# ... your build steps that produce ./firmware.bin ...
# Upload the APP image only. For arduino-cli that is the plain
# <sketch>.ino.bin, NOT <sketch>.ino.merged.bin / .bootloader.bin /
# .partitions.bin (those include the bootloader + partition table and
# will not fit the OTA app partition). See dashboard/artifacts docs.
- name: Upload to SimpleOTA
env:
SIMPLEOTA_BASE_URL: ${{ secrets.SIMPLEOTA_BASE_URL }}
SIMPLEOTA_PROJECT_ID: ${{ secrets.SIMPLEOTA_PROJECT_ID }}
SIMPLEOTA_TOKEN: ${{ secrets.SIMPLEOTA_TOKEN }}
run: |
MANIFEST_JSON=$(cat <<EOF
{
"framework": "arduino", "version_label": "${{ github.ref_name }}", "chip_family": "esp32",
"board_id": "esp32dev",
"partition_profile": "default_4mb",
"nvs_schema_version": 1,
"security_mode": "basic"
}
EOF
)
curl --fail-with-body \
-X POST "${SIMPLEOTA_BASE_URL}/api/v1/projects/${SIMPLEOTA_PROJECT_ID}/artifacts/" \
-H "Authorization: Bearer ${SIMPLEOTA_TOKEN}" \
-F "manifest=${MANIFEST_JSON}" \
-F "[email protected]"
Signed uploads¶
For projects using signed firmware, CI signs the final binary with your project's private key and includes the signature in the manifest. Add one more secret:
| Secret | Value |
|---|---|
SIMPLEOTA_SIGNING_KEY |
The private signing key PEM (shown once at key creation). |
Then replace the upload step with:
- name: Sign firmware (SimpleOTA Ed25519)
env:
SIMPLEOTA_SIGNING_KEY: ${{ secrets.SIMPLEOTA_SIGNING_KEY }}
run: |
printf '%s' "$SIMPLEOTA_SIGNING_KEY" > private.pem
openssl pkeyutl -sign -inkey private.pem -rawin -in firmware.bin \
| openssl base64 -A > firmware.sig.b64
rm -f private.pem
- name: Upload to SimpleOTA (signed)
env:
SIMPLEOTA_BASE_URL: ${{ secrets.SIMPLEOTA_BASE_URL }}
SIMPLEOTA_PROJECT_ID: ${{ secrets.SIMPLEOTA_PROJECT_ID }}
SIMPLEOTA_TOKEN: ${{ secrets.SIMPLEOTA_TOKEN }}
run: |
MANIFEST_JSON=$(cat <<EOF
{
"framework": "arduino",
"version_label": "${{ github.ref_name }}",
"chip_family": "esp32",
"board_id": "esp32dev",
"partition_profile": "default_4mb",
"nvs_schema_version": 1,
"security_mode": "signed",
"signing_metadata": {
"key_id": "prod-2026",
"algorithm": "ed25519",
"signature": "$(cat firmware.sig.b64)"
}
}
EOF
)
curl --fail-with-body \
-X POST "${SIMPLEOTA_BASE_URL}/api/v1/projects/${SIMPLEOTA_PROJECT_ID}/artifacts/" \
-H "Authorization: Bearer ${SIMPLEOTA_TOKEN}" \
-F "manifest=${MANIFEST_JSON}" \
-F "[email protected]"
Replace prod-2026 with your project's key id. The signature must cover
the final, shippable bytes: if your build also uses ESP32 Secure Boot,
run the SimpleOTA signing step after espsecure appends its signature
block (see Secure Boot notes).
The server verifies the signature at upload; a wrong key or stale
signature fails the job with a clear error.
Auto-creating a deployment¶
The example template stops at upload; promotion to a deployment is a separate, deliberate action. If you want one-click "upload + canary at 5%", follow the upload step with:
ARTIFACT_ID=$(... extract from previous response ...)
curl --fail-with-body \
-X POST "${SIMPLEOTA_BASE_URL}/api/v1/projects/${SIMPLEOTA_PROJECT_ID}/deployments/" \
-H "Authorization: Bearer ${SIMPLEOTA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"artifact\": \"${ARTIFACT_ID}\",
\"channels\": [\"beta\"],
\"percentage\": 5
}"
We deliberately leave promotion manual by default. See Promoting deployments for the recommended workflow.