PlatformIO: one upload per environment
PlatformIO installs each environment’s libraries into its own folder. You upload platformio.ini and the manifests of one environment, with their folder paths, so Declarable knows which environment each version belongs to.
Uploading from CI or the API is part of the Maker plan and the ones above it. On the Free plan the Settings page shows no key form. Without a key you can still run the free SBOM check and upload a release by hand in the app.
Sign in, open your product, then Settings → API keys. Create a key there — it is shown once. That same page shows these steps again with your product id already filled in, so once the key exists you may not need this page at all.
Install one environment’s libraries
PlatformIO writes each installed library into
.pio/libdeps/<environment>/<Library>/, with alibrary.jsonor alibrary.propertiesthat states the version that was installed.platformio.inialone only states what you asked for, often a range.shpio pkg install -e esp32devSend the files with their paths
The examples below set
DECLARABLE_KEEP_PATHS, which makes the upload script send every file under its path inside the project, such as.pio/libdeps/esp32dev/ArduinoJson/library.json, instead of its bare file name. Without the path a manifest belongs to no environment, and Declarable cannot tell esp32dev’s version of a library from another environment’s. Run the script from the project folder; a file outside it stops the job.Change
esp32devto your environment’s name everywhere it appears. The upload stops when a pattern matches no file. Most registry libraries carry alibrary.json; if none of yours carries alibrary.properties, delete that pattern, and the other way round.What one upload covers
- One environment. The versions come from that environment’s manifests only. A manifest from one environment never fills in the version of a library in another, so a second environment is a second upload with its own version label (for example
v1.4.0-esp32s3), which is a second release. - At most 10 files. That is platformio.ini and up to 9 manifests. A longer list is refused and nothing is stored. A library that has both a library.json and a library.properties uses two places, so send one manifest per library when you are near the limit. An environment with more than 9 libraries cannot be sent in one upload today.
- Not read. Source files,
.pio/build, the toolchain and board packages thatplatformpulls in, and any file outside the manifests and platformio.ini. A library with no manifest in the upload keeps the version platformio.ini gives it: a pinned version is kept, a range is shown as not pinned. Nothing is looked up online or guessed.
- One environment. The versions come from that environment’s manifests only. A manifest from one environment never fills in the version of a library in another, so a second environment is a second upload with its own version label (for example
GitHub Actions example
An example to copy and adapt, not your actual workflow file. It ends with the Declarable Action, whose two files you download from the files page.
on: push
jobs:
sbom:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: |
pip install platformio
pio pkg install -e esp32dev
# The Action is not on the Marketplace yet. Download action.yml and upload.mjs from
# https://declarable.io/docs/ci/files/action.yml and https://declarable.io/docs/ci/files/upload.mjs
# into .github/actions/declarable/ in your repository and commit them: a vendored copy is code
# you can read before it runs with your API key.
- uses: ./.github/actions/declarable
# A release is recorded for a tag and for the default branch only. Edit this line to change that.
if: github.ref_type == 'tag' || github.ref_name == github.event.repository.default_branch
with:
api-key: ${{ secrets.DECLARABLE_API_KEY }}
product-id: <your product id>
sbom-path: platformio.ini .pio/libdeps/esp32dev/*/library.json .pio/libdeps/esp32dev/*/library.properties
keep-paths: "true"
Pip, PlatformIO and the commands above were read from PlatformIO’s documentation and have not been run in this example.
GitLab CI example
An example to copy and adapt, not your actual .gitlab-ci.yml. It ends with the Declarable job.
The job needs declarable.yml and a copy of upload.mjs in your repository, both on the files page.
include:
- local: integrations/gitlab-ci/declarable.yml
build:
image: python:3.12
script:
- pip install platformio
- pio pkg install -e esp32dev
artifacts:
paths:
- platformio.ini
- .pio/libdeps/esp32dev/
declarable_upload:
extends: .declarable_upload
needs:
- job: build
artifacts: true
variables:
# The path of the upload.mjs you copied into your repository.
DECLARABLE_UPLOAD_SCRIPT: "ci/declarable/upload.mjs"
DECLARABLE_PRODUCT_ID: "<your product id>"
DECLARABLE_SBOM_PATH: "platformio.ini .pio/libdeps/esp32dev/*/library.json .pio/libdeps/esp32dev/*/library.properties"
DECLARABLE_KEEP_PATHS: "true"
Any other CI
A release script or another CI can run the same upload from a shell, from the project folder.
Read upload.sh before you run it. When paths are kept, a path with a space, a ; or a , in it stops the script.
export DECLARABLE_API_KEY=... # from your secret store, never typed into a script
export DECLARABLE_PRODUCT_ID="<your product id>"
export DECLARABLE_SBOM_PATH="platformio.ini .pio/libdeps/esp32dev/*/library.json .pio/libdeps/esp32dev/*/library.properties"
export DECLARABLE_KEEP_PATHS=true
sh integrations/curl/upload.sh
Left unset, the version is the commit the build ran from; set DECLARABLE_VERSION to the tag for a tag build. The other settings (thresholds, exit codes) are the same as on every guide; see the table on the Buildroot guide.
A worked example: build fields and outputs
One pipeline that hashes the image it built, sends the hash and the run id with the upload, and reads back the release’s address. Both fields are stored as sent; nothing checks them against the files you uploaded.
- name: Hash the firmware image
id: image
run: echo "sha256=$(sha256sum build/firmware.bin | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
- uses: ./.github/actions/declarable
id: declarable
with:
api-key: ${{ secrets.DECLARABLE_API_KEY }}
product-id: <your product id>
sbom-path: build/*.spdx
build-id: ${{ github.run_id }}
image-sha256: ${{ steps.image.outputs.sha256 }}
- run: echo "Recorded at ${{ steps.declarable.outputs.release-url }}"
The same two fields on GitLab, computed in before_script so the export reaches the job’s own script::
declarable_upload:
extends: .declarable_upload
needs:
- job: build
artifacts: true
variables:
DECLARABLE_UPLOAD_SCRIPT: "ci/declarable/upload.mjs"
DECLARABLE_PRODUCT_ID: "<your product id>"
DECLARABLE_SBOM_PATH: "build/spdx/*.spdx"
DECLARABLE_BUILD_ID: $CI_PIPELINE_ID
before_script:
- export DECLARABLE_IMAGE_SHA256=$(sha256sum build/firmware.bin | cut -d' ' -f1)
And from a shell:
export DECLARABLE_API_KEY=...
export DECLARABLE_PRODUCT_ID="<your product id>"
export DECLARABLE_SBOM_PATH="build/spdx/*.spdx"
export DECLARABLE_BUILD_ID="$CI_JOB_ID" # or any run id your own CI exposes
export DECLARABLE_IMAGE_SHA256=$(sha256sum build/firmware.bin | cut -d' ' -f1)
sh integrations/curl/upload.sh
Declarable prepares documentation for you to review. It is not legal advice and not certification. You sign your own declaration of conformity.