docs/deepnote-cli-ci.md
Rama main · réplica de lectura del repositorio original.
---
title: Running notebooks in CI with the Deepnote CLI
description: Run Deepnote notebooks without an editor in GitHub Actions or any other CI system with deepnote run, on the runner or in Deepnote Cloud, sync a repository to Deepnote, and keep your API key safe while doing it
noIndex: false
noContent: false
---
`deepnote run` executes a notebook without an editor, prints a machine-readable result and exits
non-zero when a block fails. That is everything a CI step needs, so running notebooks in GitHub
Actions, GitLab CI or any other pipeline needs no plugin: install the CLI and call it.
There are two ways to do it, and they suit different jobs:
| | On the runner | In Deepnote Cloud |
| ------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------- |
| Command | `deepnote run my-project.deepnote` | `deepnote run my-project.deepnote --cloud` |
| Where the code runs | The CI runner's Python | A separate Deepnote machine with the project's machine type, environment and integrations |
| Needs an API key | No | Yes |
| Good for | "This notebook still runs" checks on every PR | Scheduled reports, refreshing outputs, runs that need your data |
| Safe on forked PRs | Yes, nothing secret is involved | No, see [Security](#security) |
## Prerequisites
- **The Deepnote CLI.** `npx @deepnote/cli` runs it without a global install. It fetches the latest
release on every run; to stop a new release from changing a pipeline's behavior under you, pin
one with `npx @deepnote/cli@<version>`. The CLI
is also on PyPI as `deepnote-cli` if the runner has Python but no Node.js.
- **For runs on the runner:** a Python interpreter with
[deepnote-toolkit](https://pypi.org/project/deepnote-toolkit/) installed
(`pip install "deepnote-toolkit[server]"`), plus whatever packages the notebook imports. The CLI
does not install the toolkit for you.
- **For runs in Deepnote Cloud:** an API key from a workspace on a plan that includes the
[Deepnote API](/docs/deepnote-api), and a project in that workspace that holds the notebook.
## Run on the runner
This workflow runs a notebook on every pull request and fails the check if any block raises:
```yaml
# .github/workflows/notebooks.yml
name: Notebooks
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
run:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: ">=3.10 <3.15" # newest Python deepnote-toolkit supports
- uses: actions/setup-node@v7
with:
node-version: lts/*
- name: Install deepnote-toolkit and the notebook's dependencies
run: |
python -m pip install "deepnote-toolkit[server]"
python -m pip install -r requirements.txt
- name: Lint
run: npx @deepnote/cli lint analysis.deepnote --python "$(command -v python)"
- name: Run
run: |
npx @deepnote/cli run analysis.deepnote \
--python "$(command -v python)" \
--startup-timeout 60 --block-timeout 300 \
-o json > run-result.json
- name: Keep the result
if: always()
uses: actions/upload-artifact@v7
with:
name: run-result
path: run-result.json
```
A few things this relies on:
- **Which Python runs the notebook.** The CLI uses `--python <path>` if given, then the
`DEEPNOTE_PYTHON` environment variable, then an interpreter selected in the Deepnote editor
extension, then a `.venv` or `venv` with `deepnote-toolkit` installed in the notebook's folder or a
folder above it, and
finally the system Python. The workflow passes `--python` so the run always uses the interpreter
`actions/setup-python` provided, the one the dependencies were installed into, even if the
repository also contains a `.venv` or an editor's interpreter setting.
- **Timeouts.** `--startup-timeout` bounds how long the toolkit server and the kernel may take to
start; `--block-timeout` interrupts any single block that runs longer. Set both in CI so a stuck
notebook fails in minutes instead of running until the job's own timeout.
- **Inputs.** Pass values for input blocks with `--input name=value` (repeatable). Run
`deepnote run analysis.deepnote --list-inputs` locally to see which inputs a notebook has.
- **Integrations.** Database blocks read their connections from `.deepnote.env.yaml` and secrets
from `.env`, both next to the notebook (or in the directory given with `--cwd`). Generate those
files in a step from CI secrets rather than committing them, and only on events that are allowed to
see secrets.
### Reading the result
With `-o json`, the command prints one JSON object to stdout and exits with:
| Code | Meaning |
| ---- | -------------------------------------------------------------------------------------------------------- |
| `0` | Every block ran successfully |
| `1` | A block failed, or the runtime could not start |
| `2` | Invalid usage: a missing file, an unknown input, or an invalid option value. An unknown option exits `1` |
On failure, `failureCategory` says why, so a pipeline can tell a bug in the notebook from a problem
with the runner:
| `failureCategory` | Meaning |
| ------------------- | ---------------------------------------------------------------------------------------------- |
| `in-block` | The block's own code raised |
| `execution-timeout` | A block ran longer than `--block-timeout` and was interrupted |
| `kernel-died` | The kernel ended while a block ran or while starting, usually out of memory or a native crash |
| `server-exited` | The deepnote-toolkit server went away mid-run |
| `server-launch` | The server never became ready; most often `deepnote-toolkit` is not installed for that Python |
| `kernel-launch` | The server started but the kernel could not start, or was not ready within `--startup-timeout` |
When the CLI knows the remedy, the result also carries a `hint`, for example the exact
`pip install` command when the toolkit is missing.
## Run in Deepnote Cloud
`--cloud` asks Deepnote to run the notebook on a separate machine with the project's machine type,
environment and integrations, waits for it to finish and
downloads the resulting snapshot (the notebook with its outputs) into a `snapshots/` directory
next to the source file, or to the exact path given with `--out`.
```yaml
# .github/workflows/nightly-report.yml
name: Nightly report
on:
schedule:
- cron: "0 6 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
run:
runs-on: ubuntu-latest
timeout-minutes: 30
environment: deepnote # holds the DEEPNOTE_TOKEN secret; see Security below
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: lts/*
- name: Run in Deepnote Cloud
env:
DEEPNOTE_TOKEN: ${{ secrets.DEEPNOTE_TOKEN }}
run: |
npx @deepnote/cli run report.deepnote --cloud \
--storage-mode readonly --timeout 1200 \
--out report.snapshot.deepnote -o json
```
- **Which notebook runs.** The CLI reads the project and notebook ids from the `.deepnote` file. If
the file has several notebooks, choose one with `--notebook "<name>"`. Without a local file, pass
`--notebook-id <uuid>` instead, but `--input` then cannot be used, because typing input values
needs the notebook's input blocks from the file.
- **What code runs.** By default Deepnote runs the notebook as it was last saved in Deepnote, not the
file in your repository. To run what is in the repository, add `--push --yes`: `--push` replaces
the notebook's blocks in Deepnote with the file's (deleting blocks the file does not have) and
`--yes` confirms that without a prompt. Without `--yes`, `--push` refuses to run in CI.
- **Storage.** A full-notebook cloud run is detached: it does not change the outputs in the live
editor. `--storage-mode readonly` also makes the project's persistent storage read-only for that
run. Databases, integrations and external APIs stay live either way.
- **Timeouts.** `--timeout` (default 600 seconds) is how long the CLI waits. When it expires the
command fails, but the run may still be executing in Deepnote.
- **Exit codes.** `0` when the run succeeded and its snapshot was downloaded, `1` when the run failed
or the snapshot could not be fetched, and `2` for a missing or rejected token or bad arguments.
## Keep Deepnote in step with your repository
A pipeline can do more with the CLI than run notebooks. A job that runs
[`deepnote sync`](/docs/deepnote-cli-sync) keeps your repository and Deepnote in step, and is an
alternative to [Deepnote file sync](/docs/deepnote-file-sync), the Git sync built into Deepnote:
| | Deepnote file sync | `deepnote sync` in CI |
| ------------------------ | ------------------------------------------------------------------------- | ------------------------------------ |
| Set up in | Each project, with the repository connected in Deepnote | One workflow in your repository |
| Scope | One project per link | Every project in the sync directory |
| Repository → Deepnote | When someone pulls or switches branch in the Deepnote project | On every merge to the branch |
| Deepnote → repository | When someone commits and pushes from Deepnote | Committed by the job on its schedule |
| Git hosts | Ones Deepnote connects to: [GitHub](/docs/github), [GitLab](/docs/gitlab) | Any host your CI can push to |
| Needs a Deepnote API key | No | Yes |
| Branches | Switch branch in the project | Follows one branch |
Choose file sync when people work on a project's branches inside Deepnote; choose a sync job when the
repository is the source of truth and changes should land in Deepnote on merge without anyone
pulling them.
The job works on a sync directory committed to the repository: one directory per project holding
its `.deepnote` files, and `.deepnote-sync.json` at the root. Create it once from your machine with
`deepnote sync ./workspace` and commit it. Sync never creates cloud projects, so a new project has
to exist in Deepnote before a sync can track it.
```yaml
# .github/workflows/deepnote-sync.yml
name: Sync with Deepnote
on:
push:
branches: [main]
paths: ["workspace/**"]
schedule:
- cron: "0 * * * *" # also bring edits made in Deepnote back into the repository
workflow_dispatch:
permissions:
contents: write
concurrency:
group: deepnote-sync
jobs:
sync:
if: github.ref == 'refs/heads/main' # a manual run from another branch would sync that branch
runs-on: ubuntu-latest
timeout-minutes: 20
environment: deepnote # holds the DEEPNOTE_TOKEN secret; see Security below
steps:
- uses: actions/checkout@v7
with:
ref: main # the latest main, not the commit that queued this run
- uses: actions/setup-node@v7
with:
node-version: lts/*
- name: Sync with Deepnote
env:
DEEPNOTE_TOKEN: ${{ secrets.DEEPNOTE_TOKEN }}
run: npx @deepnote/cli sync workspace --on-conflict skip -o json > sync-result.json
- name: Commit what sync wrote
if: ${{ !cancelled() }}
run: |
git add workspace
if git diff --cached --quiet; then exit 0; fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -m "Sync with Deepnote"
git pull --rebase origin main
git push origin HEAD:main
- name: Fail if a project was skipped as a conflict
run: jq -e '[.projects[] | select(.action == "skipped-conflict")] | length == 0' sync-result.json
```
- **Which way each project goes.** Sync compares the local files and Deepnote's copy against the
content recorded in `.deepnote-sync.json` at the last sync. A project changed only in the
repository is pushed, one changed only in Deepnote is pulled, and one changed on both sides is a
conflict. A push is also rejected as a conflict if the project changed in Deepnote since that
record, so it never silently overwrites an edit.
- **Commit what sync writes.** After pushing a project, sync rewrites its files with Deepnote's
export and updates `.deepnote-sync.json`. If the job does not commit both, the next run compares
against the old record, sees both sides changed, and skips the project as a conflict, and keeps
skipping it. The commit step runs even when sync fails, because projects that did sync still need
their record kept. A commit pushed with the workflow's `GITHUB_TOKEN` does not start another
workflow run, so the job does not loop. If branch protection blocks direct pushes to `main`, allow
this workflow to bypass it, or the commit step fails. If a merge changes the same project while
the job runs, the rebase stops and nothing is pushed; the next run then reports that project as a
conflict, which you resolve as below.
- **Conflicts fail the job.** In CI, sync skips conflicts and still exits `0`, so the last step
checks the JSON result. To resolve one, run `deepnote sync ./workspace` on your machine and keep
the Deepnote version when it asks, re-apply your change on top, and merge it; the next run pushes
it.
- **Deletions are opt-in.** A notebook file removed from the repository is deleted in Deepnote only
with `--delete-missing-notebooks`. With `--all-files`, working-directory files such as
`requirements.txt` are uploaded when their project's notebooks are pushed, so a commit that changes
only those files does not reach Deepnote. Files removed locally are never deleted in Deepnote.
## Security
<Callout status="warning">
A Deepnote API key gives access to its whole workspace. In CI, anyone who can change the code a job
runs can read the secrets that job is given, and a cloud run executes notebook code with the
project's integrations and data.
</Callout>
Running on the runner involves no Deepnote credentials, so it is safe on any event, including pull
requests from forks. Treat cloud runs, and runs on the runner that are given database secrets, as
deployments:
- **Run them only on trusted events.** Use `push` to a protected branch, `schedule` or
`workflow_dispatch`. Never give the token to `pull_request_target` or `workflow_run` jobs that check
out or execute code from a pull request: that hands the key to whoever opened it. GitHub does not
pass secrets to `pull_request` runs from forks, so the cloud step fails there instead. Run it
conditionally rather than working around that.
- **Be deliberate with `--push` and `deepnote sync`.** Both upload the repository's version of
notebooks into your workspace, and `--push` also runs it. Whoever can merge to the branch that
triggers the job can change, or run code in, your Deepnote projects.
- **Keep the secret in a GitHub Environment** with deployment branch rules (and required reviewers if
you want a human to approve each run), rather than as a repository-wide secret, and expose it as
`DEEPNOTE_TOKEN` on the one step that needs it.
- **Limit what the key can reach.** Create it in a workspace that holds only the projects and
integrations the job needs, and prefer `--storage-mode readonly` unless the notebook must write to
project storage.
- **Mind what you upload.** The downloaded snapshot contains the notebook's outputs. On a public
repository, workflow artifacts can be downloaded by anyone who can see the repository, so do not
upload snapshots of notebooks that print private data.
- **Pin what you run.** Pin the CLI version and, ideally, third-party actions to a commit SHA.
- **Rotate and revoke** the key under **Settings & members → Security → API keys** if it is ever
exposed.
## Other CI systems
Nothing above is specific to GitHub Actions. On any CI system, install Python with
`deepnote-toolkit[server]` and Node.js (or `pip install deepnote-cli`), then call the same
`deepnote run` command; for cloud runs, expose the API key as `DEEPNOTE_TOKEN` from the system's
secret store on protected branches only.
## Troubleshooting
| Symptom | Fix |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `server-launch` with `deepnote-toolkit is not installed` | Install `deepnote-toolkit[server]` into the Python the CLI picked, or point at another with `--python` |
| `server-launch` or `kernel-launch` on a slow runner | Raise `--startup-timeout` |
| `ModuleNotFoundError` in a block | Install the notebook's own dependencies in the same Python as the toolkit |
| Cloud run exits `2` mentioning the token | The secret is not set on that step, or the key is invalid or revoked |
| Cloud run exits `1` with `Notebook not found` | The key belongs to a different workspace than the project |
| `Timed out waiting for Deepnote run` | Raise `--timeout`; the run may have finished in Deepnote anyway |
| `--push deletes blocks … needs confirmation` | Add `--yes`, after making sure the job only runs on trusted events |
## Related
- [Deepnote CLI](/docs/deepnote-cli): install the CLI and see every command
- [Deepnote API](/docs/deepnote-api): the HTTP API that `run --cloud` uses
- [Scheduling](/docs/scheduling): run notebooks on a schedule inside Deepnote, without CI
- [Syncing a workspace with the Deepnote CLI](/docs/deepnote-cli-sync)