Skip to content

CI/CD integration

Integrating ONEKEY into your development lifecycle lets you automatically analyze firmware as part of your CI/CD processes. By incorporating firmware analysis into your CI/CD pipelines, you can:

  • Automatically test builds and identify security issues early.
  • Detect and report security regressions before deployment.
  • Enforce security policies and compliance requirements.
  • Generate security reports as part of your build artifacts.
  • Block deployments with critical security vulnerabilities.

There are many ways to integrate ONEKEY depending on your use case. This guide covers some of the common approaches and best practices.

Prerequisites

Installation

Install the ONEKEY Python client in your CI/CD environment:

pip install onekey-client

Authentication

Provide credentials via API tokens:

export ONEKEY_TOKEN="your-api-token"
export ONEKEY_TENANT_NAME="your-tenant"

Warning

Never hardcode credentials in pipeline configuration files or commit them to version control. Use your CI/CD platform's secret management features (GitLab: CI/CD variables, GitHub: Secrets, Jenkins: credentials) or an external secret vault like AWS Secrets Manager, Hashicorp Vault, or 1Password.

CI/CD workflow

A typical CI/CD integration follows this pattern:

  1. Build firmware — Compile your firmware as part of the build stage.
  2. Upload firmware — Use onekey upload-firmware to upload the firmware binary.
  3. Wait for analysis and check results — Use onekey ci-result to poll analysis status, compare with previous firmware, and fail the build if new issues are found.
  4. Generate reports — Create JUnit XML reports for CI/CD integration.

Basic example

#!/bin/bash
set -e

# Upload firmware
FIRMWARE_ID=$(onekey upload-firmware \
  --product "MyDevice" \
  --vendor "MyCompany" \
  --version "1.0" \
  firmware.bin)

echo "Uploaded firmware: $FIRMWARE_ID"

# Wait for analysis and check results
onekey ci-result \
  --firmware-id "$FIRMWARE_ID" \
  --junit-path ./test-results/security.xml \
  --exit-code-on-new-finding 1

echo "Analysis passed - no new security findings"

GitLab integration

Store credentials

In your GitLab project, go to Settings → CI/CD → Variables and add two variables:

Key Value Options
ONEKEY_TOKEN Your API token Masked
ONEKEY_TENANT_NAME Your tenant name -

Masked prevents values from appearing in job logs.

Build firmware

Place the job in a stage that runs after your firmware build. The image: python:3.11 provides a clean environment with pip available:

onekey-security-analysis:
  stage: test
  image: python:3.11

Upload firmware

Install the client and upload the firmware binary. The command prints a UUID that identifies the uploaded firmware. $CI_COMMIT_SHA pins the version to the current commit:

onekey-security-analysis:
  stage: test
  image: python:3.11
  before_script:
    - pip install onekey-client
  script:
    - |
      FIRMWARE_ID=$(onekey upload-firmware \
        --product "MyDevice" \
        --vendor "MyCompany" \
        --version "$CI_COMMIT_SHA" \
        firmware.bin)

Wait for analysis and check results

Add ci-result to the same shell block so $FIRMWARE_ID is in scope. It polls until analysis completes, compares against the previous firmware for the same product, and exits non-zero if new findings appear:

onekey-security-analysis:
  stage: test
  image: python:3.11
  before_script:
    - pip install onekey-client
  script:
    - |
      FIRMWARE_ID=$(onekey upload-firmware \
        --product "MyDevice" \
        --vendor "MyCompany" \
        --version "$CI_COMMIT_SHA" \
        firmware.bin)
      onekey ci-result \
        --firmware-id "$FIRMWARE_ID" \
        --exit-code-on-new-finding 1

Generate reports

Add --junit-path to write the report, create the output directory in before_script, and configure artifacts to collect the file. when: always ensures the report is collected even when the job exits non-zero. The rules block limits execution to the default branch and merge request pipelines:

onekey-security-analysis:
  stage: test
  image: python:3.11
  before_script:
    - pip install onekey-client
    - mkdir -p junit-report
  script:
    - |
      FIRMWARE_ID=$(onekey upload-firmware \
        --product "MyDevice" \
        --vendor "MyCompany" \
        --version "$CI_COMMIT_SHA" \
        firmware.bin)
      onekey ci-result \
        --firmware-id "$FIRMWARE_ID" \
        --junit-path junit-report/security.xml \
        --exit-code-on-new-finding 1
  artifacts:
    when: always
    reports:
      junit: junit-report/security.xml
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

View results

GitLab displays findings in two places:

  • Merge request → Tests tab — the JUnit report appears alongside other test results, with individual findings listed as test cases
  • Pipeline view — a failed job signals that new security findings were detected

GitHub integration

Store credentials

In your GitHub repository, go to Settings → Secrets and variables → Actions and add two repository secrets:

  • ONEKEY_TOKEN — your API token
  • ONEKEY_TENANT_NAME — your tenant name

Build firmware

Create .github/workflows/onekey.yml. The on block triggers on pushes and pull requests to main. The checkout step makes the repository – including your firmware binary – available to subsequent steps:

name: ONEKEY Security Analysis

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  security-analysis:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

Upload firmware

Set up Python, install the client, and upload the firmware. Writing to $GITHUB_OUTPUT makes the firmware ID available to subsequent steps. The id: upload field is required to reference this output as steps.upload.outputs.firmware_id:

name: ONEKEY Security Analysis

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  security-analysis:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install ONEKEY client
        run: pip install onekey-client

      - name: Upload firmware
        id: upload
        env:
          ONEKEY_TOKEN: ${{ secrets.ONEKEY_TOKEN }}
          ONEKEY_TENANT_NAME: ${{ secrets.ONEKEY_TENANT_NAME }}
        run: |
          FIRMWARE_ID=$(onekey upload-firmware \
            --product "MyDevice" \
            --vendor "MyCompany" \
            --version "${{ github.sha }}" \
            firmware.bin)
          echo "firmware_id=$FIRMWARE_ID" >> "$GITHUB_OUTPUT"

Wait for analysis and check results

Add a step that runs ci-result with the firmware ID from the previous step. It polls until analysis completes, compares results against the previous firmware, and exits non-zero if new findings appear:

name: ONEKEY Security Analysis

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  security-analysis:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install ONEKEY client
        run: pip install onekey-client

      - name: Upload firmware
        id: upload
        env:
          ONEKEY_TOKEN: ${{ secrets.ONEKEY_TOKEN }}
          ONEKEY_TENANT_NAME: ${{ secrets.ONEKEY_TENANT_NAME }}
        run: |
          FIRMWARE_ID=$(onekey upload-firmware \
            --product "MyDevice" \
            --vendor "MyCompany" \
            --version "${{ github.sha }}" \
            firmware.bin)
          echo "firmware_id=$FIRMWARE_ID" >> "$GITHUB_OUTPUT"

      - name: Wait for analysis and check results
        env:
          ONEKEY_TOKEN: ${{ secrets.ONEKEY_TOKEN }}
          ONEKEY_TENANT_NAME: ${{ secrets.ONEKEY_TENANT_NAME }}
        run: |
          mkdir -p test-results
          onekey ci-result \
            --firmware-id "${{ steps.upload.outputs.firmware_id }}" \
            --exit-code-on-new-finding 1

Generate reports

Add --junit-path to write the report and a final step to publish it. if: always() ensures the report is uploaded even when ci-result exits non-zero due to new findings:

name: ONEKEY Security Analysis

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  security-analysis:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install ONEKEY client
        run: pip install onekey-client

      - name: Upload firmware
        id: upload
        env:
          ONEKEY_TOKEN: ${{ secrets.ONEKEY_TOKEN }}
          ONEKEY_TENANT_NAME: ${{ secrets.ONEKEY_TENANT_NAME }}
        run: |
          FIRMWARE_ID=$(onekey upload-firmware \
            --product "MyDevice" \
            --vendor "MyCompany" \
            --version "${{ github.sha }}" \
            firmware.bin)
          echo "firmware_id=$FIRMWARE_ID" >> "$GITHUB_OUTPUT"

      - name: Wait for analysis and check results
        env:
          ONEKEY_TOKEN: ${{ secrets.ONEKEY_TOKEN }}
          ONEKEY_TENANT_NAME: ${{ secrets.ONEKEY_TENANT_NAME }}
        run: |
          mkdir -p test-results
          onekey ci-result \
            --firmware-id "${{ steps.upload.outputs.firmware_id }}" \
            --junit-path ./test-results/security.xml \
            --exit-code-on-new-finding 1

      - name: Publish test results
        uses: mikepenz/action-junit-report@v5
        if: always()
        with:
          report_paths: test-results/security.xml

View results

GitHub displays findings in two places:

  • Pull request → Checks tabmikepenz/action-junit-report annotates the PR with individual findings directly alongside the diff
  • Actions run — a failed step indicates new security findings were detected

Jenkins integration

Store credentials

In Jenkins, go to Manage Jenkins → Credentials → System → Global credentials (unrestricted) and add two Secret text entries:

  • ID onekey-api-token, value: your API token
  • ID onekey-tenant-name, value: your tenant name

The IDs are referenced in the Jenkinsfile – change them to match your naming conventions if needed.

Build firmware

Place the stage in your Jenkinsfile after your firmware build stage. withCredentials injects the stored secrets as environment variables and automatically masks their values in the build log:

stage('ONEKEY Security Analysis') {
    steps {
        withCredentials([
            string(credentialsId: 'onekey-api-token', variable: 'ONEKEY_TOKEN'),
            string(credentialsId: 'onekey-tenant-name', variable: 'ONEKEY_TENANT_NAME')
        ]) {
            sh 'pip install onekey-client'
        }
    }
}

Upload firmware

Use a script block to capture the firmware UUID from the upload command. returnStdout: true captures stdout as a Groovy string; .trim() removes the trailing newline:

stage('ONEKEY Security Analysis') {
    steps {
        withCredentials([
            string(credentialsId: 'onekey-api-token', variable: 'ONEKEY_TOKEN'),
            string(credentialsId: 'onekey-tenant-name', variable: 'ONEKEY_TENANT_NAME')
        ]) {
            sh 'pip install onekey-client'
            script {
                def firmwareId = sh(
                    script: '''
                        onekey upload-firmware \
                          --product "MyDevice" \
                          --vendor "MyCompany" \
                          --version "${GIT_COMMIT}" \
                          firmware.bin
                    ''',
                    returnStdout: true
                ).trim()
            }
        }
    }
}

Wait for analysis and check results

Add a shell block inside script to run ci-result with the captured firmwareId. It compares results against the previous firmware and exits non-zero if new findings appear:

stage('ONEKEY Security Analysis') {
    steps {
        withCredentials([
            string(credentialsId: 'onekey-api-token', variable: 'ONEKEY_TOKEN'),
            string(credentialsId: 'onekey-tenant-name', variable: 'ONEKEY_TENANT_NAME')
        ]) {
            sh 'pip install onekey-client'
            script {
                def firmwareId = sh(
                    script: '''
                        onekey upload-firmware \
                          --product "MyDevice" \
                          --vendor "MyCompany" \
                          --version "${GIT_COMMIT}" \
                          firmware.bin
                    ''',
                    returnStdout: true
                ).trim()

                sh """
                    onekey ci-result \
                      --firmware-id '${firmwareId}' \
                      --exit-code-on-new-finding 1
                """
            }
        }
    }
}

Generate reports

Add --junit-path and the output directory, then collect the report in a post block. post { always { } } runs regardless of whether the stage passed or failed:

stage('ONEKEY Security Analysis') {
    steps {
        withCredentials([
            string(credentialsId: 'onekey-api-token', variable: 'ONEKEY_TOKEN'),
            string(credentialsId: 'onekey-tenant-name', variable: 'ONEKEY_TENANT_NAME')
        ]) {
            sh 'pip install onekey-client'
            script {
                def firmwareId = sh(
                    script: '''
                        onekey upload-firmware \
                          --product "MyDevice" \
                          --vendor "MyCompany" \
                          --version "${GIT_COMMIT}" \
                          firmware.bin
                    ''',
                    returnStdout: true
                ).trim()

                sh """
                    mkdir -p test-results
                    onekey ci-result \
                      --firmware-id '${firmwareId}' \
                      --junit-path ./test-results/security.xml \
                      --exit-code-on-new-finding 1
                """
            }
        }
    }
    post {
        always {
            junit 'test-results/security.xml'
        }
    }
}

View results

Jenkins displays findings in two places:

  • Build → Test Result — a breakdown of individual security findings from the JUnit report
  • Job page — a trend chart tracks the number of findings across builds over time

Best practices

Version identification

Use meaningful version identifiers that link back to your build system:

# Git commit SHA
--version "$CI_COMMIT_SHA"

# Semantic version + build number
--version "1.2.3-build${BUILD_NUMBER}"

# Branch + timestamp
--version "main-$(date +%Y%m%d-%H%M%S)"

# Tag name for releases
--version "$CI_COMMIT_TAG"

Failure policies

Decide when to fail the build:

# Fail on any new findings (strict)
--exit-code-on-new-finding 1

# Never fail, just report (informational)
--exit-code-on-new-finding 0

For custom policies (e.g., fail only on high severity), use the GraphQL API directly. See REST and GraphQL APIs for advanced use cases.

Single job vs. separate jobs

Decide whether to run single or separate jobs.

  • Single job


    Pros:

    • Simpler to implement
    • No need to pass firmware ID between jobs

    Cons:

    • Longer overall job execution time
    • Single point of failure
  • Separate jobs


    Pros:

    • Better parallelization and resource utilization
    • Allows upload to complete quickly while analysis runs
    • More granular error handling and retry logic

    Cons:

    • Requires passing firmware ID between jobs
    • Harder to implement

Typically, a single job is easier to implement as it avoids the need to pass the firmware ID between jobs and to verify as input in each job. However, for better granularity, error handling, and resource efficiency, separate jobs are recommended.

Troubleshooting

Authentication failures

Symptom: 401 Unauthorized errors

Solutions:

  • Verify ONEKEY_TOKEN and ONEKEY_TENANT_NAME are correctly set in your CI/CD secrets.
  • Check that the token has not expired or been deleted. If it has, create a new one and update the secret.
  • Verify the tenant name is correct – it must match exactly what appears in the ONEKEY web interface.

Upload failures

Symptom: Upload fails or times out

Solutions:

  • Check firmware file size and network bandwidth.
  • Verify you have Upload permission on the product group.
  • Ensure firmware file path is correct and readable.
  • Check CI/CD runner has access to the ONEKEY API.
  • Increase timeout values if uploading large files.

Analysis timeout

Symptom: ci-result command times out waiting for analysis

Solutions:

  • Increase --check-interval and --retry-count values.
  • Check analysis status on ONEKEY web interface.
  • Verify firmware was uploaded successfully.
  • Contact support if analysis is stuck in pending state.

Missing previous firmware

Symptom: No comparison performed, only current results shown

Solutions:

  • Ensure consistent product name across uploads.
  • Verify at least one previous firmware exists for the product.
  • Check that previous upload completed successfully.

JUnit report issues

Symptom: JUnit report not generated or not recognized by CI/CD

Solutions:

  • Verify --junit-path directory exists or create it first.
  • Use relative paths from job working directory.
  • Check file permissions allow writing to the specified path.