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:
Authentication¶
Provide credentials via API tokens:
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:
- Build firmware — Compile your firmware as part of the build stage.
- Upload firmware — Use
onekey upload-firmwareto upload the firmware binary. - Wait for analysis and check results — Use
onekey ci-resultto poll analysis status, compare with previous firmware, and fail the build if new issues are found. - 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:
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 tokenONEKEY_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 tab —
mikepenz/action-junit-reportannotates 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_TOKENandONEKEY_TENANT_NAMEare 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-intervaland--retry-countvalues. - 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-pathdirectory exists or create it first. - Use relative paths from job working directory.
- Check file permissions allow writing to the specified path.