Deploying from CI means three things: authenticating with an API key instead of a browser, providing the signing key from a secret, and letting non-zero exit codes fail the job. This page covers all three, with complete configurations for GitHub Actions and GitLab CI.
The shape of a pipeline job
npm ci
npm run build
npm install -g @deploy-your-app/cli
dya login --api-key "$DEPLOY_YOUR_APP_API_KEY"
dya deploy --channel production --message "Automated deploy"
Four rules apply to every CI system:
- Use
dya login --api-key. Neverdya loginwithout flags — it opens a browser and blocks for 5 minutes before timing out. - Never
dya setup. It requires a TTY and exits1immediately in a pipeline. - The
.deployyourappfile should be committed so the checked-out working directory is already linked to the app. Otherwise rundya init --app-id <appId>before deploying. - Build your web assets before
dya deploy. The CLI bundles whatever is on disk.
Authenticating with an API key
Create a key in the dashboard under Settings > API Keys. The raw key is shown once and is not recoverable — store it in your CI secret store immediately.
The CLI reads no environment variable for credentials. There is no DYA_API_KEY; the key must be passed explicitly:
dya login --api-key "$DEPLOY_YOUR_APP_API_KEY"
The name of the shell variable is entirely your choice — DEPLOY_YOUR_APP_API_KEY is used throughout this page as a convention. Always quote the expansion so a malformed value cannot be re-parsed as flags.
dya login validates the key against the server before saving it, so an expired, revoked, or mistyped key fails the login step rather than the deploy step.
Key scopes
An API key carries a list of scopes, defaulting to * (everything the owning user can do). To scope a key down to deploying, grant exactly the permissions the pipeline uses:
| Command | Required scopes |
|---|---|
dya deploy |
app:read, bundle:upload, channel:read, deployment:create |
dya deploy --no-deploy |
app:read, bundle:upload |
dya rollback |
app:read, channel:read, deployment:rollback |
dya keys upload |
app:read, app:update |
dya channels create |
app:read, channel:create |
A missing scope fails with API key missing scope: <permission>. Scopes only narrow the key — the owning user's role is still enforced, so a key created by a viewer cannot upload regardless of its scopes.
Organization binding
An API key is bound to the organization it was created in, and using it against any other organization is rejected. Two consequences for CI:
If the key owner belongs to exactly one organization, the CLI auto-selects it and no extra step is needed.
If the key owner belongs to more than one, the CLI cannot pick one on a runner with no stored config. Add an explicit switch after login:
dya login --api-key "$DEPLOY_YOUR_APP_API_KEY" dya orgs switch --slug your-org-slug
Runners usually start from a clean home directory, so the global config does not persist between jobs and the switch must run every time.
Two-factor policies
If the organization has "require two-factor for uploads" enabled, dya deploy needs a fresh 6-digit code, which it can only prompt for on a TTY. Uploads from CI will fail with TWO_FACTOR_CODE_REQUIRED. Deploy interactively, or turn the policy off for organizations that deploy from a pipeline.
Signing keys in CI
dya-signing-private.pem must be in the working directory when dya deploy runs, or the bundle is uploaded unsigned. The server rejects unsigned uploads for any app that already has a registered signing key, so a pipeline that forgets the key fails on upload.
Store the PEM as a CI secret and write it to disk at the start of the job. Base64-encoding avoids newline mangling in secret stores that only handle single-line values:
# Locally, once:
base64 -w0 dya-signing-private.pem # paste the output into your CI secret store
# In the job, before dya deploy:
printf '%s' "$DYA_SIGNING_KEY_B64" | base64 -d > dya-signing-private.pem
chmod 600 dya-signing-private.pem
Remove it at the end of the job if the workspace is not discarded:
rm -f dya-signing-private.pem
For end-to-end encryption, dya-encryption-public.pem is a public key, so committing it to the repository is fine and it needs no secret handling. Its counterpart, dya-encryption-private.pem, belongs in your app binary and must never be present on a CI runner or uploaded anywhere.
Never store any private key in the repository, in an environment variable printed by a debug step, or in build logs.
Exit codes
Every dya command exits 0 on success and non-zero on failure. Usage errors from the argument parser and runtime failures both exit 1. --help and --version exit 0.
Both example systems below fail the job automatically on a non-zero exit:
- GitHub Actions — a
runstep executes withbash -e, so the step and the job fail on the first non-zero command. - GitLab CI — the
scriptblock runs underset -e, with the same effect.
For a hand-rolled shell script, add set -euo pipefail at the top so a failed dya login does not silently fall through to dya deploy.
GitHub Actions
name: Deploy OTA update
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Build web assets
run: npm run build
- name: Install the DeployYourApp CLI
run: npm install -g @deploy-your-app/cli
- name: Restore the signing key
env:
DYA_SIGNING_KEY_B64: ${{ secrets.DYA_SIGNING_KEY_B64 }}
run: |
printf '%s' "$DYA_SIGNING_KEY_B64" | base64 -d > dya-signing-private.pem
chmod 600 dya-signing-private.pem
- name: Deploy
env:
DEPLOY_YOUR_APP_API_KEY: ${{ secrets.DEPLOY_YOUR_APP_API_KEY }}
run: |
dya login --api-key "$DEPLOY_YOUR_APP_API_KEY"
dya deploy \
--channel production \
--version "1.0.$GITHUB_RUN_NUMBER" \
--message "$GITHUB_SHA"
- name: Remove the signing key
if: always()
run: rm -f dya-signing-private.pem
Add DEPLOY_YOUR_APP_API_KEY and DYA_SIGNING_KEY_B64 under Settings > Secrets and variables > Actions in the repository. Secrets are masked in logs, but the CLI never echoes them anyway.
To gate deploys on a manually created tag or a review, put the deploy job behind a GitHub environment with required reviewers, or trigger the workflow on release: [published] instead of push.
GitLab CI
stages:
- build
- deploy
variables:
NODE_VERSION: "20"
build:
stage: build
image: node:20
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
deploy:
stage: deploy
image: node:20
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- npm install -g @deploy-your-app/cli
- printf '%s' "$DYA_SIGNING_KEY_B64" | base64 -d > dya-signing-private.pem
- chmod 600 dya-signing-private.pem
- dya login --api-key "$DEPLOY_YOUR_APP_API_KEY"
- dya deploy --channel production --version "1.0.$CI_PIPELINE_IID" --message "$CI_COMMIT_SHA"
after_script:
- rm -f dya-signing-private.pem
Define DEPLOY_YOUR_APP_API_KEY and DYA_SIGNING_KEY_B64 under Settings > CI/CD > Variables, both as Variable type and Masked. Mark them Protected as well only if the deploy runs exclusively on protected branches or tags — a protected variable is not exposed to jobs on unprotected refs, and the job would then fail at login.
Versioning
--version accepts x.y.z where each part is one or more digits. A monotonic CI counter maps onto the patch position:
dya deploy --version "1.0.$GITHUB_RUN_NUMBER" # GitHub Actions
dya deploy --version "1.0.$CI_PIPELINE_IID" # GitLab CI
Omit --version entirely and the CLI generates YYYY.MMDD.HHMMSS from the local clock. That has second granularity, so back-to-back pipelines do not collide.
Versions are unique per app across platforms. A pipeline that deploys both a Capacitor and an Electron bundle must give them different versions, or the second upload fails with a conflict.
Channel strategies
A common setup deploys every merge to a beta channel and promotes to production on a tag:
- name: Deploy to beta
if: github.ref == 'refs/heads/main'
run: dya deploy --channel beta --version "1.0.$GITHUB_RUN_NUMBER"
- name: Deploy to production
if: startsWith(github.ref, 'refs/tags/v')
run: dya deploy --channel production --version "1.0.$GITHUB_RUN_NUMBER" --rollout 10
The channel must already exist — dya deploy does not create it, and a missing channel fails after the bundle has been uploaded. Create channels once with dya channels create --name beta, or from the dashboard.
To upload a build without releasing it, pass --no-deploy and point a channel at the bundle later from the dashboard.
Rate limits and payload limits
The server applies limits that a busy pipeline can reach:
| Limit | Value |
|---|---|
| Bundle uploads | 30 per hour |
| General API requests | 100 per minute |
| Bundle file size | 500 MB |
Exceeding a rate limit returns RATE_LIMIT_EXCEEDED and the command exits 1. Runners that share an egress IP share the budget, so a fleet of parallel pipelines can hit the upload limit sooner than the per-pipeline count suggests.
Troubleshooting
API key rejected by <url> — the key is wrong, revoked, expired, or belongs to a different server than the one the CLI is pointed at. Check --server.
No active organization. Run dya orgs switch first. — the key owner belongs to several organizations. Add dya orgs switch --slug <slug> after login.
This API key is not valid for this organization — the stored activeOrgId is not the organization the key was created in. Switch to the key's own organization.
This app has a signing key registered, but the upload is unsigned — dya-signing-private.pem was not in the working directory when dya deploy ran. Check the restore step and that it runs in the same directory as the deploy.
Version <x> already exists — the version was already uploaded for this app. Use a counter that increments per pipeline run, or omit --version.
Channel "<name>" not found — the bundle uploaded but the deployment step failed. Create the channel, then re-run.
Web assets not found — no --path, no path in .deployyourapp, and no auto-detected directory containing index.html. Confirm the build step ran and produced output in the same job.
Related pages
- Command reference — every command and flag.
- Configuration — the project file, global config, and precedence.