> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heylua.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Automate releases in CI

> Run lua-cli without prompts, authenticate with a secret, branch on exit codes, and promote from a GitHub Actions workflow behind an approval

After this guide, a push to `main` compiles and tests your agent, pushes a version, creates an [agent version](/concepts/releases-and-versions), and promotes it only after a reviewer approves. The same rules apply to scripts and coding agents: every command runs with complete flags and never waits on a prompt. For the release commands themselves, see [Release an agent to production](/ship/releasing).

*Verified against lua-cli 3.33.0.*

**Before you begin**

* A scoped [API key](/concepts/credentials) for CI, stored as a repository secret. The workflow below needs `agents:read`, `agents:write`, `automations:read`, and `automations:write`; add `workflows:read` and `workflows:write` when you push workflows, and `telephony:read` and `telephony:write` when the project defines a voice. Two steps may need `knowledge:read`, the scope that also returns production environment values: the closing `lua logs` check, and `lua test` when the tool it runs reads `Data` or `User`; grant it knowingly or drop them. Leave out `agents:manage` (it deletes versions) and `knowledge:write`. `agents:write` also lets `lua push agent` change the served persona, model, model settings, and governance at once, with no promote. A scoped key cannot create or duplicate agents (`lua init --agent-name` exits 10), so create the agent once from your own user session and commit its `lua.skill.yaml`.
* `lua.skill.yaml` committed and `.env` ignored; production secrets live on the server (`lua env production -k <KEY> -v <value>`).
* Node.js 16 or later on the runner; the workflow below uses 20.

<Steps>
  <Step title="Make every command non-interactive">
    Pass the global `--ci` flag anywhere on the line. When a command would prompt, it fails instead with `Interactive prompt required but --ci flag is set. Provide all required flags or arguments.` and exit code 1, so supply the flags a prompt would have asked for: `--name`, `--set-version`, `--force`, `-m`, `-e`.

    ```bash theme={null}
    lua push all --ci --force
    lua deploy skill --ci --name tickets --set-version latest --force
    ```

    Without `--ci`, a non-TTY runner only gets a warning and then hangs on the prompt. Some prompts are not intercepted by `--ci` and block instead; the full list is on the [CLI overview](/reference/cli/overview#global-flags). Of the commands this workflow runs: `lua test skill` without `--name`, `lua logs` without `--type`, `--name`, `--user-id`, or `--json`, and `lua deploy <type>` without `--set-version` or `--force`. `lua integrations connect` needs a browser for the provider's authorization, so run it once from your machine; `lua integrations webhooks create` prompts for nothing when you pass `--connection`, `--object`, `--event`, and `--hook-url`; `lua skills production <action>` and `lua persona sandbox <action>` (other than `view`) open a menu and exit 1 under `--ci`, so script `lua skills <action>` and `lua skills sandbox view` instead.

    <Warning>
      Under `--ci`, `lua version delete <n>` deletes without asking, even without `--force`. Keep it out of unattended jobs.
    </Warning>
  </Step>

  <Step title="Authenticate with a secret">
    The CLI reads `LUA_API_KEY` from the environment (a `.env` file in the working directory counts) before any user session or credentials file. With nothing set, every command exits 9 with ``No Lua CLI authentication found. Run `lua auth configure` or set LUA_API_KEY.``

    ```bash theme={null}
    export LUA_API_KEY="${{ secrets.LUA_API_KEY }}"
    lua status --json --ci
    ```

    `lua status --json --ci` reports `auth.source` (`environment` for the variable) and `project.agentId`, proving the runner targets the right agent. Set `LUA_NO_HINTS=1` to drop the tip blocks the CLI prints after commands; the full list is on [Environment variables](/reference/cli/environment-variables).
  </Step>

  <Step title="Branch on exit codes and JSON">
    Every command exits 0 on success and a class-specific code otherwise, printed as one stderr line `✖ <code>: <message>`; the table is on [Errors and exit codes](/reference/cli/errors-and-exit-codes). `lua sync --check` exits 1 when local code and the server differ, and `lua compile --ci` exits 1 on a compile error.

    ```bash theme={null}
    lua compile --ci || exit 1
    if ! lua sync --check; then
      echo "Server and local code differ; run lua sync locally and commit." >&2
      exit 1
    fi
    ```

    Commands with `--json` print their result on stdout: `lua status`, `lua logs`, `lua test`, `lua version list|show|diff`, `lua agents`, `lua models list`, `lua auth sessions`, `lua marketplace`, and the `lua workflows` verbs. On failure, only `lua workflows`, `lua auth sessions`, and `lua marketplace` replace the typed line with `{ "success": false, "error": { "code", "statusCode", "message", "issues" } }` on stdout; every other command keeps the `✖` line on stderr, so check the exit code before parsing stdout. `lua push`, `lua deploy`, and `lua chat` have no `--json`. Three outcomes exit 0 and must be read from the output instead: `lua push all` lists primitives that failed under `⚠️  <n> component(s) failed to push:`, `lua deploy all` under `⚠️  <n> deployments failed:`, and a tool that throws under `lua test skill` returns `{ status: 'error' }`. `lua sync --check` also registers primitives the server has never seen (without a version).
  </Step>

  <Step title="Add the GitHub Actions workflow">
    Three jobs: verify (compile, one tool test, refuse to release over a newer server version), stage (push and version create), and promote, which runs in a GitHub environment with required reviewers so the live swap waits for a human. `lua version promote` itself asks no confirmation, so those reviewers are the only approval gate.

    ```yaml .github/workflows/release.yml expandable theme={null}
    name: Release agent

    on:
      push:
        branches: [main]

    env:
      LUA_API_KEY: ${{ secrets.LUA_API_KEY }}
      LUA_NO_HINTS: "1"

    jobs:
      verify:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: 20
          - run: npm ci
          - run: npm install -g lua-cli@3.33.0
          - run: lua compile --ci
          - run: |
              lua test skill --name lookup_tickets --input '{"customerEmail":"user@example.com"}' --ci --json \
                | jq -e 'type != "object" or .status != "error"'
          - name: Refuse to release over a newer server version
            run: |
              lua status --json --ci > status.json
              jq -e '[.primitives[].diffs[] | select(.status == "behind")] | length == 0' status.json
              jq -e '[.primitives[].orphans[] | select(.critical)] | length == 0' status.json

      stage:
        needs: verify
        runs-on: ubuntu-latest
        outputs:
          version: ${{ steps.create_version.outputs.version }}
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: 20
          - run: npm ci
          - run: npm install -g lua-cli@3.33.0
          - run: |
              set -o pipefail
              lua push all --ci --force 2>&1 | tee push.log
              ! grep -q "component(s) failed to push" push.log
          - id: create_version
            run: |
              lua version create --ci -m "release ${{ github.sha }}" --commit-hash "${{ github.sha }}"
              echo "version=$(lua version list --json --limit 1 --ci | jq -r '.[0].version')" >> "$GITHUB_OUTPUT"

      promote:
        needs: stage
        runs-on: ubuntu-latest
        environment: production
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: 20
          - run: npm install -g lua-cli@3.33.0
          - run: lua version promote ${{ needs.stage.outputs.version }}
          - run: lua logs --type agent_error --limit 5 --json --ci | jq -e '.logs | length == 0'
    ```

    `lua push all` rewrites `lua.skill.yaml` on the runner; the next run derives versions from the server again, so it need not be committed back. Workflows are not part of `lua push all`; add `lua push workflow --ci --force --name <workflow-name>` and `lua workflows deploy <workflow-name> -v latest` to the stage and promote jobs when the project has any.
  </Step>

  <Step title="Verify">
    Push to `main`, approve the `promote` job in GitHub, then confirm from your machine that the active version carries the commit.

    ```bash theme={null}
    lua version list --json --limit 1 --ci
    ```

    ```text Output theme={null}
    [
      {
        "version": 9,
        "status": "active",
        "message": "Removed template Harden TPL 5",
        "createdBy": "9029d3f6-3d88-487f-a5ef-4d866059d9f6",
        "createdByEmail": "stefan@heylua.ai",
        "createdAt": "2026-09-09T17:52:52.718Z"
      }
    ]
    ```

    A version created with `--commit-hash` also carries `commitHash` in this output.
  </Step>
</Steps>

## Options you may need

### Tie versions to git automatically

`lua git connect` makes `lua push`, `lua version create|promote|delete`, and `lua pull` create a commit (and a `lua/v<n>` tag on version create) in the project's repository; `--auto-push` also pushes to a GitHub HTTPS `origin` after `lua git auth github`. See the [`lua git` reference](/reference/cli/git).

### Ship one primitive from CI

Replace the stage and promote jobs with a single gated step when a CI job owns one webhook, job, processor, or trigger: `lua push webhook --ci --force --name <webhook-name>` followed by `lua deploy webhook --ci --name <webhook-name> --set-version latest --force`. For those types `lua deploy` creates and promotes a scoped agent version. A skill deploy is reset by the next `lua version promote`, so keep the version create and promote jobs for skills; a pushed persona needs no deploy ([`lua deploy` reference](/reference/cli/deploy)).

## If it isn't working

<Accordion title="✖ error: Interactive prompt required but --ci flag is set. Provide all required flags or arguments.">
  Exit 1. The command needed a value you did not pass. For `lua push` it is usually `--name` (more than one entity of that type) or `--force`; for `lua deploy`, the type or `--name` (its version picker and confirmation block instead of failing, so pass `--set-version` and `--force`). Each reference page lists the flag behind every prompt.
</Accordion>

<Accordion title="✖ auth: No Lua CLI authentication found. Run `lua auth configure` or set LUA_API_KEY.">
  Exit 9. The secret is not reaching the step. Set `LUA_API_KEY` at the job or workflow level rather than in a single step, and confirm with `lua status --json --ci` that `auth.source` is `environment`.
</Accordion>

<Accordion title="✖ forbidden: Access denied (403): …">
  Exit 10. The key is valid but has no access to the agent in `lua.skill.yaml`, for example a key from another organization. Run `lua agents --json` with the same key to list what it can see, and issue the key from the organization that owns the agent.
</Accordion>

## Next steps

<Columns cols={2}>
  <Card title="Release an agent to production" href="/ship/releasing">What each command in the CI job does and how to roll back.</Card>
  <Card title="Errors and exit codes" href="/reference/cli/errors-and-exit-codes">Every exit code and the `--json` error envelope.</Card>
  <Card title="Environment variables" href="/reference/cli/environment-variables">`LUA_API_KEY`, `LUA_NO_HINTS`, `LUA_DEBUG`, and the rest.</Card>
  <Card title="Claude Code plugin" href="/build-with-ai/claude-code-plugin">The same gate for a coding agent working locally.</Card>
</Columns>
