Skip to content

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.

Where the key and the product id come from

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.

  1. Install one environment’s libraries

    PlatformIO writes each installed library into .pio/libdeps/<environment>/<Library>/, with a library.json or a library.properties that states the version that was installed. platformio.ini alone only states what you asked for, often a range.

    sh
    pio pkg install -e esp32dev
  2. Send 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 esp32dev to your environment’s name everywhere it appears. The upload stops when a pattern matches no file. Most registry libraries carry a library.json; if none of yours carries a library.properties, delete that pattern, and the other way round.

  3. 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 that platform pulls 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.

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.

yaml
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.

yaml
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.

sh
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.

yaml
      - 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::

yaml
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:

sh
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.