Release guide

September 18, 2026 ยท View on GitHub

This guide covers firmware signing, SDK packaging, and registry publication for maintainers who publish official ESPectre releases. Run commands from the repository root. See CONTRIBUTING.md for development and review requirements, and SDK.md for SDK installation and integration.

Firmware signing

Local development builds do not need release keys. SECURITY.md summarizes what signature verification covers.

Initial key setup

Publishing requires both GitHub Actions signing secrets to match enrolled public keys. Never use the temporary keys generated by pull-request CI for a release.

Generate the keys on a trusted workstation, in a protected directory outside the checkout. The following commands assume the directory already exists and is accessible only to its owner:

umask 077
SIGNING_DIR=/absolute/path/outside/the/repository
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out "$SIGNING_DIR/firmware-rsa.pem"
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out "$SIGNING_DIR/firmware-esp32.pem"
openssl pkey -in "$SIGNING_DIR/firmware-rsa.pem" -pubout -out "$SIGNING_DIR/firmware-rsa-public.pem"
openssl pkey -in "$SIGNING_DIR/firmware-esp32.pem" -pubout -out "$SIGNING_DIR/firmware-esp32-public.pem"
.venv/bin/python .github/scripts/firmware_signing.py --enroll "$SIGNING_DIR/firmware-rsa-public.pem"
.venv/bin/python .github/scripts/firmware_signing.py --enroll "$SIGNING_DIR/firmware-esp32-public.pem"
gh secret set FIRMWARE_SIGNING_KEY_RSA < "$SIGNING_DIR/firmware-rsa.pem"
gh secret set FIRMWARE_SIGNING_KEY_ESP32 < "$SIGNING_DIR/firmware-esp32.pem"

Commit only firmware-signing-keys.json, after independently checking the printed public-key fingerprints. The RSA-3072 key signs C3, C5, C6, S2, and S3 OTA images and the domain-tagged release catalog. The ECDSA P-256 key signs classic ESP32 applications using the v1 format to support older chip revisions.

Keep an encrypted backup of both private keys under maintainer control, separate from GitHub, and record the custodians and recovery location in the project's private operational records. Never place private keys in issues, build logs, caches, firmware artifacts, or the repository.

Protect main, develop, release tags, and changes to workflows, signing scripts, and the public-key registry. Only maintainers authorized to issue firmware should be able to change code that runs with the signing secrets. CI exposes release keys only on publication-eligible pushes and tagged releases; other builds use disposable test keys. The build wrapper keeps temporary key files outside toolchain cache and upload paths and removes them when it exits. A compromised signing job can authorize malicious firmware; GitHub Secrets is part of the release trust boundary.

Rotation and recovery

Catalog verification accepts every explicitly enrolled RSA public key. Add a replacement public key before switching the secret, and retain old public keys while their signed channels remain available. Removing an old public key makes those catalogs unverifiable.

OTA key rotation requires USB installation. The v2 software verifier trusts the running application's first signature block; adding another signature is not a supported rotation procedure. The classic ESP32 verifier embeds its verification key in the app. After a loss or compromise, provision replacement secrets, publish newly signed full images, and install them via USB. Keep the old signing key only for explicitly authorized migration work; continued acceptance of compromised keys does not restore trust.

Release validation

Publication CI checks the effective signing configuration, verifies each application signature, and checks that the full USB image contains the same signed application. Before release, validate signed upgrades, wrong-key and corrupt-image rejection, interruptions, startup recovery, and USB migration on each supported target. Native has two OTA slots and a manifest-version check, but no bootloader rollback or startup health-confirmation policy. ESPHome uses its upstream rollback behavior. See SETUP.md for establishing the initial trust chain through USB.

Hardware Secure Boot, flash encryption, and hardware anti-rollback require a separate manufacturing policy covering target-specific key custody, eFuse sequencing, and recovery. General-purpose builds leave these features disabled; the decision is recorded in 2026-09-16-sign-published-firmware-and-verify-browser-downloads.md.

SDK registry publishing

Package and validate

Install the packaging dependencies from requirements.txt and Doxygen 1.17.0, and use an empty component output directory. Build the GitHub/web bundles and registry artifact from the same source revision:

python .github/scripts/build_sdk_package.py \
  --channel release --version 3.0.0 --release-tag 3.0.0 \
  --commit "$(git rev-parse HEAD)" --output-dir .cache/sdk \
  --component-output-dir .cache/registry/espectre

The packager copies an explicit set of SDK files and stamps the same version into the manifest, SDK macros, and example dependency. It also sets the version and registry in both READMEs' installation commands and the example dependency: production for stable versions, and staging for prereleases and snapshots. It records the component name, version, and source commit in sdk-metadata.json, validates both manifests with Component Manager's Python validator, and creates the registry archive with compote component pack. The manifest links to the project homepage and repository root; source provenance is kept in the metadata file so the registry does not rewrite the repository link to a commit or subdirectory.

It writes espectre-component-<version>.tgz and component-inventory.json beside the component directory. The inventory records the archive SHA-256 and every file hash. Existing GitHub/web SDK archives retain their directory layout and manifest format. Repeating a build with the same source, version, and source timestamp produces identical archives.

The registry component includes an API.md generated from the packaged public headers and integration guide through Doxygen XML. It carries the component version and source commit, including the registry suffix for snapshots. Protocol documentation links in the README remain pinned to GitHub at the packaged source revision. The SDK guide contains its integration instructions directly and requires no website pages. The website generates its HTML API reference from its own source build and includes those pages directly in the verified Pages artifact.

The verifier checks the local archive and inventory byte for byte before contacting a registry. For registry downloads and installed components, it compares every value in the root idf_component.yml after parsing YAML: Component Manager can reorder keys or change line wrapping when packaging an upload. Scalar types, list order, and all manifest fields must match the verified local archive. All other files retain exact SHA-256 checks, including the example manifests; added or missing files fail verification.

To check an extracted artifact with ESP-IDF 5.5.5 in the current shell:

python .github/scripts/verify_sdk_component.py prepare \
  --inventory .cache/registry/component-inventory.json \
  --destination /tmp/espectre-sdk-check --target esp32c3 --profile all
python .github/scripts/verify_sdk_component.py build \
  --inventory .cache/registry/component-inventory.json \
  --destination /tmp/espectre-sdk-check --target esp32c3

Use an empty destination outside the checkout. The verifier appends the selected profile's options to the extracted example's sdkconfig.defaults. It also sets a dummy SSID so the linker retains the sensing startup; configure real credentials before a hardware test. CI uses --docker with the repository's pinned ESP-IDF 5.5.5 image and mounts only the extracted consumer directory. The six minimal targets, four individual optional groups on C3, C3 with all groups, and S2 with all groups form a twelve-build matrix. Native and ESPHome firmware builds on S2 cover the frontend-owned TinyUSB console.

The verifier checks the resolved lockfile after every build: minimal, MQTT, provisioning, and frontend support profiles must download no external stacks; Direct adds only espressif/mdns. The manifest selects mDNS through a Kconfig condition and pins it to 1.12.0 because Direct's bootstrap responder uses mdns_private.h, mdns_priv_receive_action, and other private mDNS functions. Run the Direct compile and link checks before changing that pin.

Registry setup

Before the first upload, the repository owner must complete these steps:

  1. Sign in to both staging and production, own the francescopace namespace, and create the espectre component entry in each registry.
  2. In GitHub, configure environments sdk-registry-staging and sdk-registry-production. Allow release tags (*.*.*) in both, add Branch rules for main and develop in staging, and require owner approval for the first production release. The rolling tags snapshot and snapshot-dev are publication outputs and need no environment rules.
  3. In the Espressif registries, configure trusted uploaders for repository francescopace/espectre: one staging uploader (cd.yml, with environment sdk-registry-staging) and one production uploader (cd.yml, with environment sdk-registry-production). In each Espressif trusted uploader, leave the Branch field empty; GitHub environment rules restrict the allowed refs. Each publisher's filename identifies the workflow that requests the OIDC token. Replace the previous snapshot.yml and release.yml trusted uploaders when migrating to unified delivery. Follow Espressif's OIDC setup, and verify the uploader configuration with the first staging run.
  4. Confirm the right to redistribute every packaged contribution under the commercial agreement as well as GPLv3. Review the packaged source history, including renames, against CLA.md and the signatures recorded in cla-signatures.json; a DCO trailer alone does not grant commercial relicensing rights. Resolve any uncovered historical contribution before offering it under that agreement.
  5. Complete the owner review of the registry terms, including sections 5.1โ€“5.5. Section 5.3 recognizes the supplied license; seek clarification from Espressif if the broader wording of section 5.5 leaves unresolved concerns. The workflow cannot establish these legal rights or accept the terms for the owner.

CI and publication

ci.yml runs on every branch and tag push, on pull requests targeting develop, and on manual dispatches. Prepare Build, Website Unit Tests, C++ Unit Tests, and Python Unit Tests run in parallel with identical downstream dependencies. Preparation selects the version, artifact names, signing mode, and publication channel. All four jobs must succeed before the ESPHome, Matter, and Native firmware matrices and Build SDK Package start in parallel. Each successful firmware matrix starts its frontend's dependency audit, while the SDK package starts the isolated SDK component verification matrix. The publication channel passes through the SDK package and verification outputs, so Dispatch Publication depends only on the three audits and SDK verification. Dispatch Publication starts cd.yml through workflow_dispatch on the original branch or tag and links its run in the CI summary. CD derives the publication channel, release tag, registry, and environment from the validated source, then forwards that metadata through the job outputs. Firmware and SDK artifacts are built once; the publisher downloads them from that CI run. Checks and publication have separate run statuses and graphs.

Branch protection should require preparation, the initial test jobs, firmware builds, dependency audits, SDK packaging, and SDK component build checks. Downstream checks alone are insufficient because they can be skipped after an upstream failure. Snapshot and release dispatch require every terminal audit and SDK verification prerequisite to succeed. A failed CI run leaves published artifacts and coverage badges unchanged.

Registry packaging and Native and Matter container builds share the Component Manager version pinned in requirements.txt, which is compatible with ESP-IDF 5.5.5. Their Python dependency caches include the installation script in the cache fingerprint, and the IDF dependency check must pass before an installation is marked reusable. The official upload action manages its dependencies in a separate container.

EventPublication
Push to maincd.yml: preview snapshot and staging SDK
Push to developcd.yml: development snapshot and staging SDK
Push of a version tag, such as 3.0.0 or 3.0.0-rc1cd.yml: tagged release and registry verification
Other branch or tag pushes, pull requests, and manual CI runsChecks only, with disposable firmware signing keys

Release tag candidates must pass the existing semantic-version and finalized-changelog checks, and their commits must be reachable from main, before CI can use publication signing keys. The tag run supplies the required CI checks and artifacts for both stable releases and prereleases.

After Publish SDK succeeds, verify-sdk runs the same twelve-build matrix for every publication: stable tags use production, while snapshots and prerelease tags use staging. Each build prepares the example from the validated CI package with --package-example, resolves the exact SDK version from the selected registry during compilation, and compares the installed version and files against the CI package. CI uses the same example with a local SDK override. A failed verification build fails the CD run.

Snapshot workflow

On pushes to main or develop, the final dispatch-publication job in ci.yml waits for every build, test, audit, and SDK component check, then dispatches cd.yml on the source branch with the CI run ID, attempt, and full commit SHA. Firmware, GitHub releases, website updates, registry uploads, and coverage badge updates require all prerequisite jobs to succeed. Coverage badges update only after the rolling release succeeds and remain at their last published values if CI fails. Pull requests, other branches, and manually dispatched CI runs do not publish snapshots.

For snapshots, CD starts with Validate Source Run, which waits for the source CI attempt to finish and verifies its repository, push event, branch, commit, completed CI result, SDK package result, and successful dispatch job. Only Create Release depends directly on this validation. A superseded source commit skips the release job and downstream publishers. Once the release succeeds, SDK staging publication, website publication, and coverage badge publication run in parallel, using the validated source metadata forwarded through the release outputs. SDK registry verification follows the SDK upload. The SDK publisher downloads the existing registry-component artifact and uploads it to staging through OIDC. It checks that the source commit is still current, that the archive and inventory agree, and that the component belongs to the validated commit and channel. A push to develop publishes snapshot-dev; a push to main publishes snapshot and updates the website. Both branches upload SDK snapshots to staging.

To retry a failed publication, use Re-run failed jobs on the CD run while the source CI artifacts are retained. Retry failed checks on the original CI run; its final dispatch supplies the new attempt. A publisher rejects an older attempt after CI has been rerun. The original branch and commit remain the publication source, and superseded commits are skipped. Push CI runs on these branches and on tags, and publication runs, finish without automatic cancellation by newer runs; pending runs may be superseded. Other CI runs retain automatic cancellation.

Registry snapshot versions append .main or .develop to the git-describe version, for example 3.0.0-rc2-4-g0150604.main. The preview channel maps to .main. A branch push at an exact stable tag uses <tag>-0-g<short-sha>.<branch>, such as 3.0.0-0-g0150604.main, to remain a valid prerelease version; release-tag publication keeps the exact tag. The full source commit remains in the component metadata and inventory and is checked against the validated CI run. The component manifest, SDK version macros, example dependency, and inventory carry the same registry version. GitHub and website SDK bundles retain their git-describe version and rolling asset names. Repeating an upload succeeds only if the existing registry contents pass the same manifest and file comparisons against the CI artifact.

The publish job ends after the upload action confirms processing, then the shared SDK verification matrix runs against staging. Website and coverage badge publication run alongside the registry jobs after the GitHub snapshot release succeeds. A publication or registry verification failure fails that run; the originating CI retains its check results. Snapshots never upload to the production registry.

To consume a snapshot, take its exact version from component-inventory.json in the registry-component artifact, and set that version and registry_url: https://components-staging.espressif.com on the francescopace/espectre dependency in your project's idf_component.yml. Scope this URL to the SDK dependency. The SDK manifest explicitly sets the production registry for mDNS; without that field, staging can associate transitive dependencies with its own registry, where the required versions may be unavailable. Each build checks that enabled external dependencies resolve from production.

Registry builds disable the HTTP cache inside the build container and retry for up to ten minutes if the resolver cannot yet find the exact SDK version. This covers index propagation differences between runners after the upload succeeds. Other dependency errors, compiler failures, and installed-content mismatches fail immediately.

Staging snapshot retention

sdk-staging-cleanup.yml runs every Monday at 03:23 UTC on main. It only deletes old snapshots of francescopace/espectre from https://components-staging.espressif.com; the registry and component are fixed in the script. It retains the ten most recently uploaded snapshots of main and the ten most recently uploaded snapshots of develop, regardless of their age. Selection uses registry upload timestamps, not semantic version order. Both .main/.develop versions and legacy -snapshot.preview.g<sha>/-snapshot.develop.g<sha> versions are recognized. Stable versions and release candidates without a snapshot suffix are retained. GitHub releases, artifacts, and the production registry are unaffected.

Before enabling scheduled cleanup, create a staging registry API token with write:components permission and save it as the SDK_REGISTRY_STAGING_CLEANUP_TOKEN secret in the GitHub sdk-registry-staging environment. Manage its expiration and rotation in the staging token settings. Cleanup uses the API-token deletion path verified on staging; it does not assume that the publication OIDC integration permits deletion. No additional trusted uploader is required. The workflow must reach the default branch (main) before GitHub schedules it, and the environment must allow that branch.

Run SDK Staging Cleanup manually with dry_run: true (the default) to review the candidate list without deleting anything. Set keep_per_branch to the number of latest snapshots to retain for each branch (a positive integer, default 10). Scheduled runs apply the policy automatically with ten snapshots per branch; a manual run with dry_run: false applies the selected count. The Actions summary and JSON report record the candidates and completed deletions, including partial progress if a deletion fails. A read-only local preview requires no token:

python .github/scripts/cleanup_sdk_registry.py --report /tmp/sdk-staging-cleanup.json

Deletion is permanent, and a deleted version number cannot be uploaded again, even on staging. Consumers pinned to a deleted snapshot must select a retained version before resolving dependencies again. Keep release-tag validation versions outside this rolling retention policy.

Release workflow

On a validated release tag, CI builds firmware and SDK artifacts with the exact tag as their version. After all checks pass, it dispatches cd.yml on that tag with the CI run ID, attempt, and full commit SHA. Validate Source Run verifies the successful completed source CI attempt, SDK package job, and dispatch job, and revalidates the tag and main ancestry. Create Release then publishes the validated artifacts, signed firmware manifests, compliance bundles, registry archive, and changelog notes to GitHub.

After Create Release succeeds, SDK publication, website publication, and coverage badge publication run in parallel. The SDK verification matrix depends only on Publish SDK. The validated source run ID and registry URL pass through the release and upload outputs, so verification uses the published registry and original tag CI artifacts. Coverage badges are attached to the versioned GitHub release. Tagged release publication does not require a separate snapshot run on main.

Stable tags publish directly to the production registry after the GitHub release succeeds. Publish SDK uploads the final-version archive already built and verified by tag CI, using the pinned official Espressif action and OIDC, then the shared verification matrix builds all twelve consumers against production.

Prerelease tags, such as 3.0.0-rc1, follow the same flow with staging as the SDK registry. Other dependencies come from production. Prereleases never upload to production. The upload job compares an existing version before skipping upload; differing contents fail the run. Registry resolution retries have a bounded wait, so propagation delays can be retried without silently accepting a different package. A registry verification failure fails the CD run; the already published GitHub release, website, and badges are retained.

Retry a failed release publication through Re-run failed jobs on the CD run while its source CI artifacts are retained. A failed publication does not change the successful CI result; follow the publication link in the CI summary for its outcome.

Website deployment

CD dispatches pages.yml on main after building and verifying the website, and wait for the deployment result. The separate branch run avoids the existing Pages issue with tag deployments reusing an older artifact for the same commit. It validates the source publication workflow, attempt, main ancestry, and completed prerequisite jobs, then downloads the Pages archive and sitemap from the successful website build attempt. A superseded snapshot website is rejected.

The serialized Pages job uploads the verified website archive unchanged, including the API reference generated by that build. Each build replaces the generated reference tree and verifies that it contains exactly its current page inventory. The deployment checks the live channel catalogs and signing keys before notifying IndexNow.

For a website-only retry, rerun the failed Pages job, or dispatch pages.yml on main with the CD run ID as pages_run_id and its current attempt as pages_run_attempt. This does not publish firmware or registry components.

Migrating the workflow configuration

Install cd.yml and pages.yml on the default branch (main) before relying on automatic dispatches from develop or version tags. GitHub requires dispatched workflow files to exist on the default branch, and the selected ref must contain the new workflow definitions and validation script. Carry the configuration through the normal protected-branch review process, update the registry trusted uploaders described above, and retire snapshot.yml and release.yml as part of the same migration. The Pages validator accepts only artifacts from cd.yml; reruns that use the previous workflow definitions must be completed before switching. Environment branch and tag rules remain unchanged.

Ensure the tag points to the intended commit on main and has a finalized changelog entry. Its own CI run must succeed before release publication starts.

Keep the ROADMAP.md publication gate open until public installation succeeds and the C3 hardware check covers startup, calibration, movement, and Wi-Fi reconnection.