CI/CD Integration

Run the DeployYourApp CLI in a pipeline with an API key, handle signing keys and secrets, and fail builds on non-zero exit codes.


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. Never dya login without flags — it opens a browser and blocks for 5 minutes before timing out.
  • Never dya setup. It requires a TTY and exits 1 immediately in a pipeline.
  • The .deployyourapp file should be committed so the checked-out working directory is already linked to the app. Otherwise run dya 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 run step executes with bash -e, so the step and the job fail on the first non-zero command.
  • GitLab CI — the script block runs under set -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 unsigneddya-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.


Previous
Configuration
Next
Setup