This is a monorepo to deploy Jupyter or IDE types of application to the Cloud. It consists in several packages, all managed as uv workspace members.
Code: ./libs/jupyter-deploy
CLI tool for deploying Jupyter server to the cloud.
It's cloud-provider and infrastructure-as-code agnostic. The CLI code MUST NOT:
- depend directly on any cloud provider-specific libraries (e.g.
boto3for AWS) - assume that an infrastructure-as-code engine is selected (e.g. it MUST remain extensible to other engines than
terraform) - create custom dataclasses for AWS API types; use boto3 type stubs directly (e.g.
ObjectTypeDef,TagTypeDef)
To access cloud-provider specific dependencies, we use optional installs such as pip install "jupyter-deploy[aws]"
Then module provider/instruction_runner_factory handles these optional imports.
You MUST NOT break that pattern with import statements to cloud-provider or infrastructure-as-code specific libraries
outside of the instruction runner code paths.
1. Core Layer (handlers, engine, provider):
- Handlers provide abstraction for each
jupyter-deploycommands - Raise exceptions from
jupyter_deploy/exceptions.pyfor errors - Accept
DisplayManagerinstance from CLI, then use its method:info(),warning(),success(),hint() - Defines
SupervisedExecutorclass for managing infrastructure-as-code subprocesses- Located in
engine/supervised_execution - Emits progress events that
DisplayManagerhandles - Supports switching between
stdinandstdoutwhen subprocess prompts for input
- Located in
- Defines the abstract, provider-agnostic command runner:
/provider/manifest_command_runner- Run commands declared in a template manifest
- Use a specific provider module (e.g.
/provider/aws), which calls provider-SDK (e.g.boto3) - Optional install thanks to lazy import in factory module
/provider/instruction_runner_factory
2. Provider and Engine Implementations (unified abstraction):
- Engine implement specific command Handlers for a specific infrastructure-as-code engine; current engines:
terraform
- Engine {Config|Up|Down}Handlers leverage
SupervisedExecutorto run the infrastructure-as-code subprocess calls - Provider instruction runners implement the
InstructionRunnerinterface to make API calls with the specific provider SDK; current providers:aws
3. CLI Layer (cli/):
- Instantiate Console and error handler
- Call core handlers and catch exceptions
- Format and display results using rich/typer
- Implement
DisplayManagerprotocol with display managers; implementations:SimpleDisplayManager(cli/simple_display.py) - Spinners, status messages for SDK-style operationsProgressDisplayManager(cli/progress_display.py) - Progress bars, log boxes for long operationsNullDisplay(engine/supervised_execution.py) - No-op for programmatic/test usage
- Exception Handling: All custom exceptions in
jupyter_deploy/exceptions.py - Keep Core Generic: Core defines interfaces, instantiates engine-specific and provider-specific instance as needed
- No Terminal-specific Dependencies in Core: rich/typer only in cli/ module
- No Engine-specific implementation in Core: use the
/engine/<engine-name>module - Not Provider-specific implementation in Core: use the
/provider/<provider-name>or/api/<api-name>modules
Code: ./libs/jupyter-deploy-tf-ec2-base
Primary template used by the CLI, referred to as "base template".
- infrastructure-as-code engine:
terraform - cloud provider:
aws - identity provider:
github
All variables MUST be defined in variables.tf without default values.
Default values MUST be set in presets/defaults-all.tfvars.
There MUST NOT BE be any variable blocks in files other than variables.tf.
IMPORTANT: Do not copy files to /home/jovyan during Docker build time.
The EBS volume for Jupyter data is mounted at runtime, and any files copied during build will be hidden by this mount.
Instead, copy files to a location like /opt during build and then copy them to /home/jovyan in startup scripts.
Code: ./libs/jupyter-deploy-tf-aws-eks-oidc
- infrastructure-as-code engine:
terraform - cloud provider:
aws - identity provider:
github(via Dex OIDC)
The engine directory has three tiers — keep them separate:
- Core infra (
modules/,main.tf,iam.tf,eks_addons.tf,platform.tf): VPC, IAM roles, EKS cluster, MNG, security groups. Usemodules/for reusable resources. These must exist before a kubeconfig is available. - Platform components (
platform_*.tf): Helm charts deployed onto the cluster once a working MNG is available — one file per component, namedplatform_<component>.tf(e.g.platform_karpenter.tf,platform_keda.tf,platform_logging.tf). Never put Helm releases inmodules/. - App/charts (
helm.tf,workspaces.tf): consumer charts and workspace-specific resources (operator, router, workspace-defaults).
All variables MUST be defined in variables.tf. Default values MUST be in presets/defaults-all.tfvars. No variable blocks elsewhere.
All local-exec provisioners MUST set interpreter = ["/bin/bash", "-c"] — Terraform defaults to /bin/sh.
With bootstrap_cluster_creator_admin_permissions = false, the caller's IAM role MUST be listed in admin_role_names to retain cluster access. A check block validates this at plan time.
Destroy order is load-bearing and enforced via depends_on (see eks_addons.tf/iam_role/vpc comments): VPC+roles → DaemonSet addons (CNI/kube-proxy) → node groups → Deployment addons (coredns/ebs-csi/…) → Helm releases → workspaces, so the operator stays alive through Helm uninstalls.
Do NOT bump the version in the local charts' Chart.yaml (charts/*/Chart.yaml) when editing chart contents. Only bump versions with the versioning script.
Code: ./libs/jupyter-infra-tf-aws-iam-ci
Template that manages AWS resources for GitHub Actions CI.
- infrastructure-as-code engine:
terraform - cloud provider:
aws - no host/server resources — IAM roles, SSM parameters and secrets only
IMPORTANT: The GitHub Actions OIDC provider is a singleton per AWS account.
The create_oidc_provider variable controls whether to create it or reference an existing one.
Set to false if another deployment in the same account already created it.
Code: ./libs/pytest-jupyter-deploy
A set of pytest fixtures to run end-to-end tests for templates, referred to as "pytest plugin".
It bundles the E2E container image (Dockerfile + docker-compose.yml) used by the justfile to run E2E tests. The image is template-independent — it provides base tooling (Python, Terraform, AWS CLI, Playwright) while template-specific tests are synced at runtime.
Always run from the root of the repository:
- Run linting and formatting:
just lint- Runs
ruff format,ruff check --fix,mypy,terraform fmt, andyamllint
- Runs
- Run unit tests:
just unit-test- Runs
uv run pytest
- Runs
Open PRs are reviewed automatically in CI by roborev (policy in .roborev.toml). Run the same review locally with just review; see CONTRIBUTING.md.
- you MUST NOT silence linters without the user's permission
- you MUST NOT write docstrings that merely repeat a method name
- you MUST NOT use
TYPE_CHECKINGimports anywhere
In CLI command docstrings and help strings (libs/jupyter-deploy/jupyter_deploy/cli/):
- Avoid mentioning "jupyter", "jupyterlab", or "jupyter-deploy project" — use "project" or "app".
- Reference commands with angle brackets:
<jd init>,<jd up>. - Reference optional flags bare (no backticks): --overwrite or -o.
- Reference flag values in lowercase angle brackets: --path , --variable .
Note: Rules 2-4 DO NOT apply to
console.print()statements, only to cli docstrings.
Unit tests are located in libs/<package-name>/tests/unit
- Define
unittest.TestCaseinstance for each class, function or major method to be tested - you SHOULD NOT use
pytest.fixtures - Use
@patch()or inlinewith patchwhen possible - Always set
: Mocktyping formypywith patches - When mocking boto3 types in tests, use proper type annotations (e.g.,
instance_state: InstanceStateTypeDef = {"Code": code}) rather than casting - If you detect inconsistencies between implementation and test assertions (e.g., code raises
KeyErrorbut test expectsValueError), notify the user of the implementation issue rather than modifying the unit tests to pass
Smoke tests live in libs/jupyter-deploy/tests/e2e/. No browser interaction, no deployed template — pure CLI validation.
They run inside a container built from .github/e2e-cli/.
Three variants, each installing a different dependency set and running a matching test track:
- bare — CLI only; tests validate that
boto3is NOT installed - aws —
jupyter-deploy[aws]+ base template; runs the aws installation tests - aws-k8s —
jupyter-deploy[aws,k8s]+ base template; runs the aws AND k8s installation tests
Examples:
- aws (workspace code):
just ci-e2e-cli-build && just test-smoke-cli aws jupyter-deploy-e2e-cli:latest - bare (published PyPI):
just test-smoke-cli bare— auto-builds a pypi image; tests validate thatboto3is NOT installed - bare (from Test PyPI):
just ci-e2e-cli-build "" "--build-arg INSTALL_MODE=pypi --build-arg INSTALL_VARIANT=bare --build-arg PKG_VERSION=<version> --build-arg EXTRA_INDEX_URL=https://test.pypi.org/simple/" && just test-smoke-cli bare jupyter-deploy-e2e-cli:latest
Pre-publish gate: release-cli.yml runs all three smoke variants from the locally-built wheel (via a file:// flat uv index, INDEX_FORMAT=flat) BEFORE publishing to Test PyPI. This catches packaging-metadata, missing-file, and test-selector regressions without burning a version number. The post-publish gate (e2e-cli.yml) re-runs the same three variants against the actual Test PyPI install.
E2E tests are located in libs/<template-name>/tests/e2e/
- Use the pytest plugin for test fixtures and helpers
- Use
@skip_if_testvars_not_set([...])decorator to skip tests when required env vars are missing - Template-specific utilities should be in
test_utils.pywithin the template's e2e directory - Template-specific fixtures should be in
conftest.pywithin the template's e2e directory
E2E tests validate a complete deployment with actual CLI commands and browser-based interactions using playwright.
The E2E tests run in a local container using pytest where playwright and webbrowsers are installed.
just e2e-upbuilds and starts the container (image frompytest-jupyter-deployplugin).just e2e-syncsynchronizes the workspace files with the container.- Per-template convenience wrappers:
just test-e2e-base,just test-e2e-eks-oidc. - Generic commands accept a
templateparameter:just test-e2e <project-dir> <filter> <options> <template>. Look at./justfilefor more details.
IMPORTANT: you CANNOT run any e2e directly with uv run pytest E2E-TEST-SELECTOR, you MUST use a just command.
IMPORTANT: Deployment directories contain their own copy of template files.
When testing template changes in an existing deployment (e.g., sandbox-e2e),
you must manually copy the modified template files from libs/jupyter-deploy-tf-aws-ec2-base/jupyter_deploy_tf_aws_ec2_base/template/
to the deployment directory BEFORE running configuration tests or deploying.
IMPORTANT: E2E tests MUST be run sequentially — never run multiple just test-e2e commands in parallel.
- Browser-auth tests require a restored CI project providing bot credentials:
just ci-restore sandbox-ci— restores the CI project from the store
- A deployed project to test against located in a dir relative to the workspace root:
- e.g.
./sandbox - or restore one with
just ci-restore-base <oauth-app-num> sandbox-ci <project-dir>
- e.g.
- Some E2E tests require environment variables to be set:
- look at
./env.examplein the workspace root - the user must have created an
.envfile with values at the workspace root - tests will be skipped if the required test environment variables are not set.
- generate the
.envfrom the CI project with:just env-setup-base <project-dir> sandbox-ci <oauth-app-num>
- look at
The configuration test verifies a template project is correctly wired up.
In the case of the base template, this corresponds to the terraform plan operation succeeding.
Before running configuration tests on modified template files:
- Copy changed template files from base template to the test deployment directory
- The configuration test validates the LOCAL files in the deployment directory, not the installed package
To run the configuration test:
- ask the user for the
<project-dir>to use - if testing modified template files, ensure they've been copied to
<project-dir> - run
just test-e2e-base <project-dir> test_project_is_configurable
Note: test_configuration contains many additional error-recovery tests. Use test_project_is_configurable for a quick validation that the template works.
Run E2E tests against an existing deployment: just test-e2e-base <project-dir> TEST-SELECTOR
Prerequisite: Tests that authenticate a browser (most tests except test_configuration) require a restored CI project providing bot credentials. Pass ci-dir=<ci-project> in options:
Examples (for project-dir == sandbox3, ci-dir == sandbox-ci):
- Run all E2E tests without mutating the project:
just test-e2e-base sandbox3 "" ci-dir=sandbox-ci - Run all E2E tests:
just test-e2e-base sandbox3 "" mutate=true,ci-dir=sandbox-ci - Run specific test file:
just test-e2e-base sandbox3 test_users ci-dir=sandbox-ci(possibly needsmutate=true) - Run config-only test (no ci-dir needed):
just test-e2e-base sandbox3 test_project_is_configurable - Run CI template E2E tests:
just test-e2e-ci sandbox3 ""
NOTE: mutate tests are long, pipe to log stream to file: just test-e2e <project-dir> TEST-SELECTOR mutate=true,ci-dir=sandbox-ci 2>&1 | tee results.log
The test container saves screenshots of failed tests to ./test-results, use the read image tool.
Run E2E tests: just test-e2e-eks-oidc <project-dir> TEST-SELECTOR
Prerequisite: Same as base — pass ci-dir=<ci-project> for browser-auth tests.
Examples (for project-dir == sandbox-e2e, ci-dir == sandbox-ci):
- Run all tests:
just test-e2e-eks-oidc sandbox-e2e "" mutate=true,full-deploy=true,ci-dir=sandbox-ci - Run workspace tests only:
just test-e2e-eks-oidc sandbox-e2e test_workspace mutate=true,full-deploy=true,ci-dir=sandbox-ci
Gotcha: JD_E2E_RBAC_TEAM in .env MUST match the team in oauth_allowed_teams that the sandbox was deployed with.
The RBAC RoleBinding on the cluster grants workspace access to that group — if they don't match, all impersonation-based workspace tests will fail with Forbidden.
These operations mutate live cloud infrastructure. Follow them exactly.
IMPORTANT: You MUST ALWAYS let a jd up or jd down run to completion.
Never interrupt, kill, cancel, or time out an in-progress jd up/jd down (or the
underlying terraform process). A full EKS deploy/destroy can take 20-30 minutes;
run it in the background and wait for it to finish rather than aborting. Interrupting
an apply mid-run is how partial/duplicate deployments and orphaned resources happen.
IMPORTANT: The correct resume sequence after a failed jd up is jd config THEN jd up.
Never run jd up alone to resume a failed deployment — it will use the stale plan from
the previous run and either fail with "Saved plan is stale" or, worse, create duplicate
infrastructure by treating the existing partial state as a fresh deployment. Always
regenerate the plan with jd config first so Terraform re-evaluates against the current
remote state, then apply with jd up.
IMPORTANT: Before retrying jd up after a mid-apply failure, check for stuck Helm releases.
A failed apply can leave Helm releases in pending-install or failed state. These block
the next apply with "cannot re-use a name that is still in use". Check with:
helm list -A | grep -E "pending|failed"
and uninstall any stuck releases before running jd config && jd up.
IMPORTANT: jd down on EKS clusters requires the correct kubeconfig.
Before running jd down, ensure kubectl is configured for the cluster being destroyed:
aws eks update-kubeconfig --name <cluster-name> --region <region>
Without this, the pre-destroy cleanup script cannot reach the cluster to uninstall Karpenter,
causing the destroy to fail with "unable to uninstall Helm release karpenter".
IMPORTANT: jd up automatically backs up the project (files + terraform state) to
the remote store (an S3 bucket) after a full run — even if the run ends in failure.
IMPORTANT: A NEW variable added to the template won't apply to an existing deployment
until you also add it to that deployment's <project-dir>/variables.yaml overrides:
block (a fresh jd init picks it up automatically; an existing project does not).
To teardown a deployment:
- confirm with the user that it's okay
- run
jd down -y(let it complete — see above) - once fully succeeded:
- remove the store entry with
jd projects delete <PROJECT-ID> --store-type s3-only - delete the local
<project-dir>
- remove the store entry with
- if you hit issues:
- see logs with
jd history show down; possibly retry cd <project-dir>/engineand try theterraformcommand directly- if all else fails, look up AWS resources by tag (
DeploymentId) via the Resource Groups Tagging API and clean them up manually. Note: tags may not capture every resource — watch for resources created by Kubernetes operators (LBs, ENIs, security groups, VPC endpoints).
- see logs with
Important: Most jd commands (init excepted) assume the cwd is a particular project; change dir, or use the --path attribute (most commands support it).
Essential commands for debugging a deploy project:
jd --helporjd CMD SUB-CMD --help- Find out about API shapesjd show --variables --list- Display list of available variablesjd show --outputs --list- Display list of available outputsjd show -v VARIABLE-NAME --text- Display the variable value (careful: it does not guarantee it was applied withjd up)jd show -o OUTPUT-NAME --text- Display the output valuejd config- Reconfigure deploymentjd up- Apply infrastructure changesjd history show CMD- Display the content of the latest CMD (configorup) run (pass-n 2for the second-to-latest, etc)jd history show up -n 100 -s 100- Display lines [-200:-100] of the latestuprun
More specific commands are template-dependent.
Read the AGENT.md in each project directory for detailed instructions.
Refer to docs/AGENT.md
Refer to diagrams/AGENT.md
Refer to .github/AGENT.md.
Ask the user the package(s) they want to update version, and whether they want to bump the major, minor or patch.
Then use the just update-version method.
Whenever you're asked to update the CLI version, upgrade the root uv.lock with uv sync --upgrade --all-packages.
Refer to .github/AGENT.md