name: Declarable SBOM upload description: Upload the build's SBOM to Declarable and fail the step if the open findings cross your thresholds. author: Declarable branding: icon: upload-cloud color: blue inputs: api-key: description: A Declarable API key with the releases:write and products:read scopes. Pass it as ${{ secrets.DECLARABLE_API_KEY }}. required: true product-id: description: The product's id (it is in the product page's address). required: true sbom-path: description: One or more SBOM files or globs (*, ?, **), separated by newlines or spaces, such as build/spdx/*.spdx. Every match is sent in one release, each file once. A path with a space in it cannot be named. required: true keep-paths: description: 'Set "true" to send each file under its path inside the project (such as .pio/libdeps/esp32dev/ArduinoJson/library.json) instead of its base name. Needed only by a reader that takes the environment from the file''s path (PlatformIO today); the step stops if a file is outside the folder it runs in.' required: false default: "false" version: description: The release's version label. Re-running with a version that already exists records nothing new and goes on to check the findings. required: false default: ${{ github.sha }} api-base-url: description: Where Declarable runs. Change it only for a self-hosted or staging copy. required: false default: https://declarable.io replace: description: 'Set "true" to replace the release already under this version when the files differ. Without it, different files under an existing version are left alone.' required: false default: "false" released-at: description: 'Optional. The day the firmware shipped, as YYYY-MM-DD, read as a UTC date. Leave it empty to send no date (the build date is not the ship date). Declarable refuses a date in the future. If these exact files were already uploaded under this version, the date is ignored.' required: false default: "" commit: description: 'The commit the firmware was built from, stored with the release. Default: the commit that triggered the workflow. Set "none" to send no commit. At most 200 characters. If these exact files were already uploaded under this version, the commit is ignored.' required: false default: ${{ github.sha }} build-id: description: 'Optional. Your own label for the build (a CI run number, say), stored with the release. At most 200 characters. Ignored when these exact files were already uploaded under this version.' required: false default: "" image-sha256: description: 'Optional. The SHA-256 of the firmware image, 64 hex characters (a leading sha256: is dropped). Stored with the release; nothing checks it against a real image. Ignored when these exact files were already uploaded under this version.' required: false default: "" supported-until: description: 'Optional. The last day you will support this release, as YYYY-MM-DD. Ignored when these exact files were already uploaded under this version.' required: false default: "" fail-on-gap: description: "Fail the step at this gap severity or worse: blocker, major, minor or none. none never fails the step. Gaps are read from the product's current release; before the first release they are reported as not checked." required: false default: blocker fail-on-exploited: description: Fail the step when an open vulnerability is known to be exploited ("true" or "false"). required: false default: "true" outputs: release-id: description: The id of the release this step uploaded, or of the release already under this version. Empty if the upload failed. value: ${{ steps.upload.outputs.release-id }} release-url: description: The address of that release in Declarable. value: ${{ steps.upload.outputs.release-url }} component-count: description: How many components the release holds. Empty when the version already exists with different files, because Declarable does not send the count then. value: ${{ steps.upload.outputs.component-count }} recorded: description: '"true" when this step recorded a new or replaced release; "false" when the version already existed (same files, or different files left alone).' value: ${{ steps.upload.outputs.recorded }} runs: using: composite steps: - name: Check that Node 18 or later is available shell: bash run: | if ! command -v node >/dev/null 2>&1; then echo "Declarable: this step needs Node 18 or later on PATH. In a container: job, add actions/setup-node before it, or upload from a second job that runs on ubuntu-latest." >&2 exit 2 fi if [ "$(node -p 'Number(process.versions.node.split(".")[0])')" -lt 18 ]; then echo "Declarable: this step needs Node 18 or later, and found $(node --version). Add actions/setup-node with node-version 18 or later before it, or upload from a second job that runs on ubuntu-latest." >&2 exit 2 fi - name: Upload the SBOM to Declarable id: upload shell: bash env: DECLARABLE_API_KEY: ${{ inputs.api-key }} DECLARABLE_PRODUCT_ID: ${{ inputs.product-id }} DECLARABLE_SBOM_PATH: ${{ inputs.sbom-path }} DECLARABLE_KEEP_PATHS: ${{ inputs.keep-paths }} DECLARABLE_VERSION: ${{ inputs.version }} DECLARABLE_API_BASE_URL: ${{ inputs.api-base-url }} DECLARABLE_REPLACE: ${{ inputs.replace }} DECLARABLE_RELEASED_AT: ${{ inputs.released-at }} DECLARABLE_COMMIT: ${{ inputs.commit }} DECLARABLE_BUILD_ID: ${{ inputs.build-id }} DECLARABLE_IMAGE_SHA256: ${{ inputs.image-sha256 }} DECLARABLE_SUPPORTED_UNTIL: ${{ inputs.supported-until }} DECLARABLE_FAIL_ON_GAP: ${{ inputs.fail-on-gap }} DECLARABLE_FAIL_ON_EXPLOITED: ${{ inputs.fail-on-exploited }} # In a container job the github.action_path expression is the host's path; the environment # variable is the mapped one (actions/runner#716; argument, no real runner run yet). # Docs read 2026-10-11: https://docs.github.com/en/actions/reference/workflows-and-actions/metadata-syntax run: node "$GITHUB_ACTION_PATH/upload.mjs"