# index.md # **Workshop** **Workshops are secure, fast, and composable development environments that come agent-ready**. **Wrap complex, error-prone workspaces into reliable and reproducible definitions of languages, libraries, and tooling**. The key pieces of a definition are SDKs: independent, connectable units of functionality that publishers package and share on the SDK Store, and teams can define in their repositories. **Workshops enable sandboxed experimentation, turn environment updates into manageable transactions, and ensure consistent, reproducible environments**. With **Workshop**, you can launch a setup that previously took hours to configure in a few commands and be sure it will work the same way every time, or tear it down and start from the last step without worrying about leftover state. **Agentic engineering, AI/ML, robotics, IoT, EdTech, and similar domains** typically use less-than-trivial project layouts that rely on many Ubuntu versions or container images, a plethora of diverse tools and frameworks, and a wide range of libraries and languages. That’s where **Workshop** thrives. **Built for AI workflows**. **Workshop** publishes [LLM-readable docs](https://ubuntu.com/workshop/docs//reference/ai-agents.md#ref-ai-discovery), and ships agentic skills for [operating workshops](https://ubuntu.com/workshop/docs//reference/ai-agents.md#ref-ai-use-workshop-skill) and [scaffolding SDKs](https://ubuntu.com/workshop/docs//reference/ai-agents.md#ref-ai-sdk-designer-skill). --- ## In this documentation | **Tutorial** | [Get started](https://ubuntu.com/workshop/docs//tutorial/part-1-get-started.md#tut-get-started) •
[Work with interfaces](https://ubuntu.com/workshop/docs//tutorial/part-2-work-with-interfaces.md#tut-interfaces) •
[Sketch SDKs](https://ubuntu.com/workshop/docs//tutorial/part-3-sketch-sdks.md#tut-sketch-sdks) •
[Craft SDKs](https://ubuntu.com/workshop/docs//tutorial/part-4-craft-sdks.md#tut-craft-sdks) | |---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Workshops** | [Concepts](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-concepts) •
[Launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) •
[Refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh) •
[Connect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connect) •
[Shell access](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-shell) •
[Add actions](https://ubuntu.com/workshop/docs//how-to/customize-workshops/add-actions.md#how-add-actions) •
[Multi-workshop patterns](https://ubuntu.com/workshop/docs//explanation/workshops/multi-workshop-patterns.md#exp-multi-workshop-patterns) •
[Use multiple workshops](https://ubuntu.com/workshop/docs//how-to/customize-workshops/use-multiple-workshops.md#how-use-multiple-workshops) •
[Forward ports](https://ubuntu.com/workshop/docs//how-to/customize-workshops/forward-ports.md#how-forward-ports) •
[Status diagrams](https://ubuntu.com/workshop/docs//reference/workshop-status.md#ref-workshop-status) •
[Definition file](https://ubuntu.com/workshop/docs//reference/definition-files/workshop-definition.md#ref-workshop-definition) | | **SDKs** | [Concepts](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-concepts) •
[Sketch SDKs in-place](https://ubuntu.com/workshop/docs//tutorial/part-3-sketch-sdks.md#tut-sketch-sdks) •
[Craft full SDKs](https://ubuntu.com/workshop/docs//tutorial/part-4-craft-sdks.md#tut-craft-sdks) •
[Parts](https://ubuntu.com/workshop/docs//explanation/sdks/parts.md#exp-sdk-parts) •
[Design best practices](https://ubuntu.com/workshop/docs//explanation/sdks/best-practices.md#exp-sdk-best-practices) •
[SDKs vs Dockerfiles](https://ubuntu.com/workshop/docs//explanation/sdks/sdk-vs-dockerfile.md#exp-dockerfile-vs-sdk) •
[Definition file](https://ubuntu.com/workshop/docs//reference/definition-files/sdk-definition.md#ref-sdk-definition) | | **Interfaces** | [Concepts](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-concepts) •
[Camera](https://ubuntu.com/workshop/docs//explanation/interfaces/camera-interface.md#exp-camera-interface) •
[Custom device](https://ubuntu.com/workshop/docs//explanation/interfaces/custom-device-interface.md#exp-custom-device-interface) •
[Desktop](https://ubuntu.com/workshop/docs//explanation/interfaces/desktop-interface.md#exp-desktop-interface) •
[GPU](https://ubuntu.com/workshop/docs//explanation/interfaces/gpu-interface.md#exp-gpu-interface) •
[Mounts](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md#exp-mount-interface) •
[SSH agent](https://ubuntu.com/workshop/docs//explanation/interfaces/ssh-interface.md#exp-ssh-interface) •
[Networking](https://ubuntu.com/workshop/docs//explanation/interfaces/tunnel-interface.md#exp-tunnel-interface) | | **Projects** | [Concepts](https://ubuntu.com/workshop/docs//explanation/workshops/projects.md#exp-projects) •
[Move projects](https://ubuntu.com/workshop/docs//how-to/customize-workshops/move-projects.md#how-move-projects) •
[Update projects](https://ubuntu.com/workshop/docs//tutorial/part-1-get-started.md#tut-project-updates) •
[Changes and tasks](https://ubuntu.com/workshop/docs//explanation/workshops/changes-tasks.md#exp-changes-tasks) | | **Development** | [Connect VS Code](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/connect-vscode.md#how-vscode-connect-remote) •
[JetBrains Gateway](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/run-jetbrains-gateway.md#how-jetbrains-gateway) •
[JupyterLab in browser](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/run-jupyterlab-in-browser.md#how-jupyterlab-run-in-browser) •
[Use with Git](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/use-git.md#how-git-workshops) •
[Run GitHub Actions locally](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/run-github-actions-locally.md#how-run-github-actions-locally) •
[AI agents](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/use-workshops-with-ai-agents.md#how-use-workshops-with-ai-agents) | | **Troubleshooting** | [Debug workshops](https://ubuntu.com/workshop/docs//how-to/fix-workshops/debug-issues.md#how-debug-issues-workshops) •
[Fix installation](https://ubuntu.com/workshop/docs//how-to/fix-workshops/fix-installation.md#how-troubleshoot) •
[Resolve plug conflicts](https://ubuntu.com/workshop/docs//how-to/fix-workshops/resolve-plug-conflicts.md#how-resolve-plug-conflicts) •
[Purge workshops](https://ubuntu.com/workshop/docs//how-to/fix-workshops/purge.md#how-purge) | | **Architecture** | [Components](https://ubuntu.com/workshop/docs//explanation/architecture/components.md#exp-arch-system-components) •
[Runtime behavior](https://ubuntu.com/workshop/docs//explanation/architecture/runtime-behavior.md#exp-arch-runtime-behavior) •
[Workshop internals](https://ubuntu.com/workshop/docs//reference/workshops.md#ref-workshop-internals) •
[SDK internals](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-sdk-internals) | | **CLI** | [Workshop CLI](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-cli) •
[SDK CLI](https://ubuntu.com/workshop/docs//reference/cli/sdk.md#ref-sdk-cli) •
[SDKcraft CLI](https://ubuntu.com/workshop/docs//reference/cli/sdkcraft.md#ref-sdkcraft-cli) •
[workshopctl CLI](https://ubuntu.com/workshop/docs//reference/cli/workshopctl.md#ref-workshopctl-cli) | ## How this documentation is organized This documentation follows the [Diátaxis documentation framework](https://diataxis.fr/), organizing content by the type of information users need. The four sections serve different purposes: [Tutorial](https://ubuntu.com/workshop/docs//tutorial/index.md): Hands-on learning path for new **Workshop** users, progressing from basic operations through interface usage to SDK development. [How-to guides](https://ubuntu.com/workshop/docs//how-to/index.md): Step-by-step instructions for specific tasks like connecting IDEs, managing projects, and troubleshooting issues. [Reference](https://ubuntu.com/workshop/docs//reference/index.md): Technical specifications for CLI commands, definition file formats, and internal behavior. [Explanation](https://ubuntu.com/workshop/docs//explanation/index.md): In-depth discussion of **Workshop** architecture, concepts, and design principles. --- ## Project and community **Workshop** is an emergent project within the DevEx department here at Canonical; **SDKcraft** is its sibling project, aimed at publishers who create and distribute SDKs for **Workshop**. At its core, **Workshop** builds upon Canonical’s mature tech. It uses [LXD](https://documentation.ubuntu.com/lxd/latest/) as the underlying container technology; it also follows the tooling paradigm exemplified by [Snap](https://snapcraft.io/docs/), and implemented with [Craft CLI](https://craft-cli.readthedocs.io/en/latest/). ### Get involved - [Contribution and participation](https://ubuntu.com/workshop/docs//contributing.md#contributing) ### Releases and roadmap - [Release notes](https://ubuntu.com/workshop/docs//release-notes/index.md#release-notes) ### Governance and policies - [Code of conduct](https://ubuntu.com/community/docs/ethos/code-of-conduct) - [Security policy](https://ubuntu.com/workshop/docs//security.md) - [License](https://github.com/canonical/workshop/blob/main/LICENSE) ### Feedback and support - [Product and documentation feedback](https://github.com/canonical/workshop/issues) # 404.md # Page not found **Sorry, but the documentation page that you are looking for was not found.** Documentation changes over time, and pages are moved around. We try to redirect you to the updated content where possible, but that didn’t work this time (perhaps, the content you were looking for does not exist in this version of the documentation). You can use the navigation to locate the content you’re looking for, or search for a similar page. ![Penguin with a question mark](/workshop/docs//404.svg) # add-actions.md # How to add actions to your workshop Actions automate mundane tasks inside an existing workshop and enhance its functionality without modifying the SDKs themselves or running lengthy **workshop exec** commands. ## Add actions To add actions, edit your `workshop.yaml` file, adding named action definitions in **bash** format under `actions` and making use of the features provided by the SDKs in your workshop Here’s an example of a workshop definition with two actions that use the capabilities provided by the sketch SDK from the [Customize with sketch SDKs](https://ubuntu.com/workshop/docs//tutorial/part-3-sketch-sdks.md#tut-sketch-sdks) tutorial section: ```yaml name: dev base: ubuntu@22.04 sdks: - name: go channel: 1.26 actions: lint: | golangci-lint run --out-format=colored-line-number -c .golangci.yaml shellcheck: | git ls-files | file --mime-type -Nnf- | grep shellscript | cut -f1 -d: | xargs shellcheck --check-sourced --external-sources ``` Unlike changes in SDK layout or base, action updates do not require a **workshop refresh**. ## Accept arguments Action bodies are **bash** scripts, and they consume the arguments that **workshop run** forwards to them as positional parameters. For instance, `"$@"` expands to every argument passed after the action name, while `"$1"`, `"$2"`, and so on pick individual ones. To add a `tests` action that forwards arbitrary flags and paths to **go test**: ```yaml name: dev base: ubuntu@22.04 sdks: - name: go channel: 1.26 actions: tests: go test "$@" ``` Keep the quotes around `"$@"`: they preserve the boundaries between arguments, so flags with spaces or wildcards reach the action intact. #### NOTE For details on **bash** positional parameters, see [Special Parameters](https://www.gnu.org/software/bash/manual/html_node/Special-Parameters.html) in the **bash** manual. ## Run actions To execute an action, use the **workshop run** command. Specify the workshop and its action, with an optional separator (`--`): ```console $ workshop run dev -- lint main.go:1: ./main.go:5:2: "os" imported and not used (typecheck) package main $ workshop run dev shellcheck In 1.sh line 10: cat /etc/passwd | grep root ^---------^ SC2002 (style): Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. ``` Any arguments supplied after the action name are forwarded to the action’s **bash** script as positional parameters. For example, the `tests` action defined above runs a single test under a specific package: ```console $ workshop run dev -- tests -run TestFoo ./pkg/... ``` In projects with a single workshop, the workshop name is optional: ```console $ workshop run -- lint ``` ## Conclusion By adding actions to your workshop, you can streamline your daily **Workshop** workflows and reduce the risk of typing errors. For more advanced scripting capabilities, consider exploring additional features of the SDKs, such as hooks. ## See also Explanation: - [Actions](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition-actions) - [SDK hooks](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks) Reference: - [workshop exec](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-exec) - [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh) - [workshop run](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-run) - [workshop actions](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-actions) # add-mounts.md # How to add mounts to a workshop **Workshop** exposes filesystem locations to a workshop through the mount interface. A plug declared on an SDK names a target directory inside the workshop; **Workshop** binds a source directory to it at run-time, either from a path **Workshop** allocates on the host or from one inside the workshop. There are five common scenarios worth discussing. ## Persist workshop-internal files on the host To persist data that the workshop produces or uses outside the project directory (tooling caches, user data, logs) without picking and maintaining a host directory yourself, add a plug under an SDK in `workshop.yaml` with `workshop-target` only: ```yaml name: dev base: ubuntu@24.04 sdks: - name: uv plugs: data: interface: mount workshop-target: /home/workshop/data ``` Refresh the workshop to bind the target: ```console $ workshop refresh ``` **Workshop** allocates a host directory for the plug at `~/.local/share/workshop/id///mount///` and binds it to `/home/workshop/data/` inside the workshop. Files written there from the workshop survive **workshop start**, **workshop stop**, and **workshop refresh** because the data lives on the host. This is the cheapest way to get persistence when the workshop, not the user, owns the lifecycle of the files. ## Remount a host directory inside the workshop If the workshop needs to consume some ad-hoc data from the host, declare the plug as before and then point it at a host path with **workshop remount**. ```yaml name: dev base: ubuntu@24.04 sdks: - name: uv plugs: shared: interface: mount workshop-target: /home/workshop/shared ``` Refresh the workshop, stop it, point the plug at the host path, then start it again: ```console $ workshop refresh $ workshop stop dev $ workshop remount dev/uv:shared ~/datasets $ workshop start dev ``` The host path can be absolute or relative. **Workshop** only swaps a live mount atomically when the new source is non-existent or empty. For a populated source like `~/datasets/`, the workshop must be stopped first to avoid corrupting in-flight reads or writes, hence the **workshop stop** and **workshop start** above. Inside the workshop, `~/datasets/` from the host is now visible at `/home/workshop/shared/`. The **workshop remount** command sets up a durable share; use it for the host data the user owns and wants the workshop to access across many sessions, not just one. Once a share source is set, **workshop info** surfaces it alongside the `workshop-target`: ```console $ workshop remount dev/uv:shared ~/datasets $ workshop info dev ... sdks: uv: mounts: shared: host-source: /home/user/datasets workshop-target: /home/workshop/shared ... ``` The override survives **workshop refresh**, so the share stays in place across SDK and base updates without re-running **workshop remount**. More, **workshop remove** does not delete the remounted host directory; the data on disk is assumed to be the user’s, so **Workshop** leaves it alone. What *does* go away with the removed workshop is its *record* of the remount: a fresh **workshop launch** starts with the auto-allocated source until you remount again. To drop the override on demand without removing the workshop, see [Reset a remount](#how-reset-remount). ## Expose a host directory read-only For shared reference data, configuration, or secrets that the workshop should read but not modify, add `read-only: true` to the plug: ```yaml name: dev base: ubuntu@24.04 sdks: - name: uv plugs: readonly: interface: mount workshop-target: /home/workshop/readonly read-only: true ``` Refresh the workshop, then point the plug at the host directory. Stop the workshop first if `~/refdata/` already holds the reference data, as in the previous scenario: ```console $ workshop refresh $ workshop stop dev $ workshop remount dev/uv:readonly ~/refdata $ workshop start dev ``` Writes to `/home/workshop/readonly/` from inside the workshop fail even with `sudo`. The `mode`, `uid`, and `gid` plug attributes control the permissions and ownership of any directory that **Workshop** creates on behalf of the plug. Defaults are `1000:1000` for targets under `/home/workshop/`, `/project/`, or `/run/user/1000/`, and root otherwise. As with any remounted plug, **workshop remove** leaves the host directory in place. ## Share a directory between SDKs The mount interface also can connect SDKs in the same workshop without going through the host. The slot SDK declares `workshop-source` to publish a directory inside the workshop; the plug SDK consumes it at its `workshop-target`. For instance, the `uv` and `jupyter` SDKs ship this pattern out of the box: `uv` exposes `/home/workshop/uv-venv/` through a `venv` slot, and `jupyter` consumes it with a `venv` plug at `$SDK/venv/`. List both SDKs and wire them with an explicit `connections` entry: ```yaml name: dev base: ubuntu@24.04 sdks: - name: uv - name: jupyter connections: - plug: jupyter:venv slot: uv:venv ``` ```console $ workshop refresh ``` After the refresh, `jupyter` and `uv` share the same Python virtual environment, and neither SDK is aware of the other. ## Reset a remount To drop a custom source set with **workshop remount** and return the plug to its auto-allocated host directory, disconnect the plug with `--forget`, which discards the source override, then connect the plug to the system mount slot to re-establish it: ```console $ workshop disconnect dev/uv:shared --forget $ workshop connect dev/uv:shared :mount ``` **Workshop** then re-binds `/home/workshop/shared/` to the auto-allocated directory under `~/.local/share/workshop/`. Note that **workshop connections** now lists the plug as `manual` in the `NOTES` column. That state is sticky: it survives **workshop refresh** and **workshop stop** plus **workshop start** cycles, so the share stays in place across normal lifecycle operations. To revert connections and mounts wholesale, use **workshop restore**. For a single plug, run **workshop disconnect** with `--forget` to disconnect it and forget its manual state. ## See also Explanation: - [SDK dependencies](https://ubuntu.com/workshop/docs//explanation/sdks/best-practices.md#exp-best-dependencies) - [Mount interface](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md#exp-mount-interface) - [System SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-system-sdk) Reference: - [workshop connect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connect) - [workshop disconnect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-disconnect) - [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh) - [workshop remount](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-remount) - [workshop restore](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-restore) # ai-agents.md # Workshop and AI agents **Workshop** integrates with AI coding agents, exposing documentation as Markdown that agents can fetch and parse directly, or retrieve through Context7, and agentic skills that wrap **Workshop** and **SDKcraft** operations so agents don’t have to rediscover the CLIs every session. ## LLM-readable docs **Workshop** publishes two files that follow the [llms.txt convention](https://llmstxt.org/): [llms.txt](https://ubuntu.com/workshop/docs/llms.txt) indexes every page with a one-line summary, and [llms-full.txt](https://ubuntu.com/workshop/docs/llms-full.txt) concatenates every page as Markdown. To fetch a single page as Markdown, append `.md` to its URL. For example, this page is available at `https://ubuntu.com/workshop/docs/reference/ai-agents.md`. ## Context7 [Context7](https://context7.com/canonical/workshop) indexes the **Workshop** documentation and serves it to AI agents through its Model Context Protocol (MCP) server, so agents can pull current docs without scraping the site. ## The use-workshop skill The [use-workshop-skill](https://github.com/canonical/use-workshop-skill) repository ships an agentic skill for operating the **Workshop** CLI: launching workshops, refreshing them, running commands inside, wiring interfaces, debugging failed changes, and orchestrating parallel environments via Git worktrees. To enable it in a repository, copy `.github/skills/use-workshop/` into the target repo, using the skills path for your agent (`.claude/skills/` for Claude Code, `.github/skills/` for Copilot). Mention **Workshop** in any prompt to trigger the skill. ## The sdk-designer skill The [template-sdk](https://github.com/canonical/template-sdk) repository ships an agentic skill named `sdk-designer`. The skill runs an interactive scaffolding conversation: it asks about the software to package, the target platforms, and which interfaces and hooks are needed, then writes the corresponding files into the template. 1. Aim the agent at the new repository. 2. Run `/sdk-designer` and answer the prompts. 3. Review the generated files and adjust where the skill’s defaults don’t match your case. # architecture.md # Architecture **Workshop**’s core architecture combines a set of system components with well-defined runtime behavior. Understanding these fundamentals is key to effectively using and managing **Workshop** environments. * [System components](https://ubuntu.com/workshop/docs//explanation/architecture/components.md) * [Runtime behavior](https://ubuntu.com/workshop/docs//explanation/architecture/runtime-behavior.md) # best-practices.md # Design best practices When crafting SDKs for **Workshop**, publishers face design decisions that affect how their SDKs install, integrate, and work inside workshops. Understanding the best practices outlined below helps publishers create more maintainable, reliable, and user-friendly SDKs that better align with **Workshop**’s architecture and ideology. This explanation covers key design considerations and provides rationale for common patterns found in a number of SDKs available in the **Workshop** ecosystem. ## System services System services within SDKs should be designed to integrate smoothly with the workshop’s lifecycle and other SDK components. Consider the approach used by the `ollama` SDK: it implements a `setup-project` hook that configures and starts the **systemd** service by including a service file: ```yaml parts: user-service: plugin: dump source: ollama.service source-type: file ``` The file provides appropriate service configuration: ```ini [Unit] Description=Ollama Service After=network.target [Service] ExecStart=/bin/bash -lc "ollama serve" Restart=always RestartSec=3 [Install] WantedBy=default.target ``` And gets installed during the `setup-project` phase: ```shell install -D --mode=644 --target-directory ~/.config/systemd/user "$SDK/ollama.service" systemctl --user daemon-reload systemctl --user enable --now ollama ``` This design ensures that the service starts automatically when the workshop is launched, and stops cleanly when the workshop is terminated. ## Parts decomposition The [parts mechanism](https://ubuntu.com/workshop/docs//explanation/sdks/parts.md#exp-sdk-parts), shared by **Workshop** with projects such as [Snapcraft](https://documentation.ubuntu.com/snapcraft/stable/explanation/parts/), enables modularity by separating different aspects of an SDK into discrete, manageable components. Effective decomposition strategies depend on the SDK’s complexity and the independence of its components. Consider the `go` SDK, which uses a single part because the Go toolchain can be distributed as a cohesive unit: ```yaml parts: go: plugin: dump source: https://go.dev/dl/go$CRAFT_PROJECT_VERSION.linux-$CRAFT_ARCH_BUILD_FOR.tar.gz source-type: tar ``` In contrast, the `ollama` SDK is built with multiple parts for the runtime and service configuration, allowing selective updates and reducing build times: ```yaml parts: ollama: plugin: dump source: https://github.com/ollama/ollama/releases/download/v0.9.6/ollama-linux-amd64.tgz source-type: tar user-service: plugin: dump source: ollama.service source-type: file ``` Parts should be organized around functional boundaries: | Component Type | Description | |--------------------|---------------------------------------------------------------| | Runtime components | Core binaries and libraries that change infrequently | | Configuration | Settings and templates that may need customization | | Data assets | Large files like models or datasets that update independently | | Tools | Auxiliary utilities that complement the main functionality | However, parts are not mandatory: the minimal viable option is to forgo them entirely and install everything in the [hooks](#exp-best-parts-or-hooks). ## Interface layout Interfaces define how SDKs interact with the host system and other SDKs. The layout of interfaces ultimately impacts an SDK’s usability and security. Publishers should select interfaces based on the resources their SDK requires (via plugs) or exposes (via slots). In particular, the [mount interface](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md#exp-mount-interface) plugs are frequently used because they specifically address data persistence and sharing needs. The `uv` SDK demonstrates this by mounting `/home/workshop/.cache/uv` to preserve package caches across workshop life-cycle, improving performance for workshop refresh: ```yaml plugs: cache: interface: mount workshop-target: /home/workshop/.cache/uv ``` This configuration means that the `/home/workshop/.cache/uv/` directory inside the workshop maps to a persistent storage location on the host system. This setup allows the `uv` SDK to retain its cache between refreshes. Rather than a plug, a slot provides resources to the workshop; for instance, the `ollama` SDK uses the `tunnel` interface slot to expose its server functionality on a specific port, enabling external access to its services: ```yaml slots: ollama-server: interface: tunnel endpoint: 11434 ``` The most obvious interface choices are as follows: - Use `mount` for persistent data and caches - Use `gpu` when GPU acceleration is required - Use `tunnel` for network services that need to be accessible externally - Use `ssh` for authentication with remote services For a complete list, see [Interfaces](https://ubuntu.com/workshop/docs//explanation/index.md#exp-interfaces); for a discussion of interface capabilities, see [Interface concepts](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-concepts). ## Environment variables Environment variables provide a clean way to configure SDK behavior and integrate with workshops. SDKs should use standard POSIX-compatible shell mechanisms to add variables to the workshop. For system-wide variables that affect all users, SDKs should place configuration files in `/etc/profile.d/`. The `uv` SDK demonstrates this approach by setting system-wide `PATH` modifications in its `setup-base` hook: ```shell cat <> /etc/profile.d/uv.sh PATH="$SDK/bin:\$PATH" EOF ``` For user-specific variables that only make sense for the `workshop` user, SDKs should modify `~/.profile`. Again, the `uv` SDK illustrates this pattern by setting `UV_LINK_MODE=copy` in its `setup-project` hook to address interaction between SDK behavior and workshop architecture: ```shell cat <> ~/.profile # SDK uses 'mount' interface to preserve # uv cache across refreshes, thus, hardlinking is # not available on 'uv sync'. export UV_LINK_MODE=copy EOF ``` Publishers should avoid shell-specific configuration files, such as `~/.bash_profile` or `~/.bashrc`, because **Workshop** supports multiple shell interpreters and these files may not be sourced consistently across different shell sessions. Some guidelines for environment variables: - Use clear names that indicate origin and purpose; prefix them with the SDK name to avoid conflicts - Include comments explaining why specific values are chosen - Choose between `/etc/profile.d/` for system-wide settings and `~/.profile` for user-specific configuration ## Parts or hooks? The decision between shipping prebuilt content and, alternatively, installing it dynamically at runtime through hooks affects SDK size, startup time, and flexibility. Different content types have different optimal strategies. For instance, Debian packages are best installed in hooks, particularly in `setup-base`, because they integrate with the system package manager and can leverage **apt**’s local cache. Installing packages during SDK build would bypass distribution security updates and create larger SDK artifacts. The `ros2` SDK exemplifies this approach: ```shell apt-get update apt-get install ros-dev-tools apt-get install python3-colcon-argcomplete python3-colcon-alias python3-colcon-clean python3-colcon-mixin # ... ``` In general, binary artifacts are best shipped as parts when you need to: - Pin specific versions regardless of what’s available in package repositories, - Distribute custom builds with specialized compilation flags, - Provide tools that aren’t available through the system package manager. This approach ensures consistent environments and avoids eventual dependency conflicts. The `uv` SDK shows this approach by shipping prebuilt Rust binaries: ```yaml parts: uv: plugin: rust source: https://github.com/astral-sh/uv source-tag: $CRAFT_PROJECT_VERSION source-type: git organize: uv: bin/uv uvx: bin/uvx prime: - bin/uv - bin/uvx ``` ## `setup-base` or `setup-project`? The choice between `setup-base` and `setup-project` hooks fundamentally affects when and how SDK initialization occurs. This decision impacts performance, caching behavior, and the SDK’s presence in workshop snapshots. First of all, note that both `setup-base` and `setup-project` should configure the workshop for running, but normally don’t directly control service startup or other runtime behavior. For instance, they can configure container shutdown or startup, but they shouldn’t start services directly unless there’s a specific reason to do so. The `setup-base` hook runs once when the SDK is installed at launch or refresh time, making it ideal for system-wide configuration that doesn’t change between projects. Operations in `setup-base` become part of [workshop snapshots](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition-sdks), improving startup performance at subsequent refreshes. For instance, the `uv` SDK uses `setup-base` for system-wide configuration that includes shell completion, `PATH` updates, and system package manager integration: ```shell sudo -u workshop mkdir -p /home/workshop/uv-venv cat <> /etc/profile.d/uv.sh PATH="$SDK/bin:\$PATH" EOF "$SDK"/bin/uv generate-shell-completion bash > /etc/bash_completion.d/uv.sh "$SDK"/bin/uvx --generate-shell-completion bash > /etc/bash_completion.d/uvx.sh mkdir -p /usr/local/libexec/alternatives cat << 'EOF' > /usr/local/libexec/alternatives/uv-pip #!/bin/bash exec uv pip "$@" EOF chmod +x /usr/local/libexec/alternatives/uv-pip update-alternatives --install /usr/bin/pip pip /usr/local/libexec/alternatives/uv-pip 50 ``` The `setup-project` hook runs as the `workshop` user after `setup-base`, when interfaces are connected and the workshop is fully operational. This makes it suitable for project-specific initialization that might vary depending on the actual project files. For instance, the `comfy` SDK uses `setup-project` to detect the available GPU type, configuring the appropriate PyTorch variant accordingly: ```shell GPU_TYPE="none" if command -v lspci >/dev/null 2>&1; then if lspci | grep -i 'NVIDIA' >/dev/null 2>&1; then GPU_TYPE="nvidia" elif lspci | grep -i 'AMD/ATI' >/dev/null 2>&1; then GPU_TYPE="amd" elif lspci | grep -i 'Intel.*Graphics' >/dev/null 2>&1; then GPU_TYPE="intel" fi fi echo "Detected GPU: $GPU_TYPE" ``` The need to use `setup-project` for this purpose arises from the fact that the GPU is accessed via an auto-connected interface, so its availability can only be determined after the workshop has launched and interfaces are connected. However, the choice of packages to install depends on the GPU type, necessitating dynamic configuration at project setup time: ```shell case "$GPU_TYPE" in nvidia) pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu129 ;; amd) pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm6.4 ;; *) pip install torch torchvision torchaudio ;; esac ``` In general, you use `setup-base` for: - System package installation - Global environment configuration - One-time setup operations - Content that should be part of snapshots (e.g., infrequently updated or unlikely to change) Choose `setup-project` for: - Project-specific configuration that depends on the project context - Operations requiring [auto-connected interfaces](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-connections) - Content that shouldn’t be part of snapshots (e.g., frequently updated or extra large) ## Health checks Health check scripts provide essential feedback about SDK operational status and help users diagnose problems quickly. Well-designed health checks go beyond simple binary success or failure, reporting extra details to provide actionable diagnostic information. The `ollama` SDK demonstrates comprehensive health checking by testing actual service functionality and channeling its error output: ```shell if ! output=$(sudo -u workshop --login ollama list 2>&1); then workshopctl set-health error "$output" exit fi workshopctl set-health okay ``` When the workshop is launched with `--wait-on-error`, the **workshop info** output will contain these details. In general, health checks should: - Test each of the relevant features, not just the sheer fact of installation - Provide specific error codes for different failure modes - Include helpful error messages that guide troubleshooting with **workshop changes** - Run quickly to avoid slowing workshop operations down - Handle edge cases gracefully ## SDK dependencies The `mount` interface enables sophisticated collaboration patterns between SDKs within a workshop while avoiding explicit dependency management. Rather than having each SDK prepare and maintain its own resources, SDKs can expose capabilities they provide through slots and consume them through plugs, creating efficient resource utilization patterns. Consider Python-based SDKs that need to install packages from PyPI. Instead of each SDK maintaining its own virtual environment, one SDK can provide a shared environment that others consume. The `uv` SDK demonstrates this by exposing a virtual environment slot: ```yaml slots: venv: interface: mount workshop-source: /home/workshop/uv-venv ``` Here, `workshop-source` says that the resource is inside the workshop, rather than on the host. Other Python-based SDKs can then connect to this shared environment through corresponding plugs. The `jupyter` SDK shows this pattern: ```yaml plugs: venv: interface: mount workshop-target: $SDK/venv ``` **Workshop** users wire the two together through a `connections:` block in their workshop definition; when no compatible slot is available, the consuming SDK falls back to the host directory that **Workshop** automatically provides for the plug, so a Python-based SDK still works on its own. This pattern extends beyond Python or its virtual environments to encompass various shared resources, including common libraries and runtime environments, shared data directories and caches, and development tools and utilities. It offers several advantages: - Eliminates duplication of large tool chains or environments - Maintains separation between SDK responsibilities - Allows workshop users to mix and match compatible SDKs - Avoids the complexity of dependency management with a fallback mechanism Both examples above assume SDK publishers ship the required plugs and slots. When they don’t, the workshop user can [graft](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition-connections) the missing elements in the workshop definition, extending an SDK’s capabilities without the publisher’s involvement. This makes plug and slot management a shared effort: SDK authors define the standard capabilities, and users augment them to fit their project’s needs. ## See also Explanation: - [Interface concepts](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-concepts) - [Mount interface](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md#exp-mount-interface) - [SDK concepts](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-concepts) - [SDK hooks](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks) - [SDK parts](https://ubuntu.com/workshop/docs//explanation/sdks/parts.md#exp-sdk-parts) Reference: - [SDK definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdk-definition.md#ref-sdk-definition) - [SDK hooks](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-sdk-hooks) - [SDK parts](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-sdk-parts) - [workshop changes](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-changes) - [workshop info](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-info) - [workshop start](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-start) - [workshop stop](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-stop) # build-an-sdk.md # How to build an SDK A standalone SDK is a **SDKcraft** project with its own `sdkcraft.yaml`, its own `hooks/` tree, and the parts and interfaces that describe what it ships and how it integrates with workshops. Build one by laying out the project, declaring parts and interfaces, authoring the lifecycle hooks, and exercising the result locally before any thought of publishing. ## Prerequisites Before starting, ensure the following are in place: - **SDKcraft** is installed. - LXD 6.6 or later is running on the host. - **Workshop** is installed and configured so that you can launch a workshop to try the SDK against. ## Start from the template New SDKs start from [canonical/template-sdk](https://github.com/canonical/template-sdk), a GitHub-template repository that ships the project skeleton: - a `sdkcraft.yaml` ready to be filled in, - a `hooks/` tree with stubs for the lifecycle hooks, - a `VERSION` file pinning the upstream release the SDK wraps, - a `renovate.json` that tracks upstream releases on a long-lived version branch and opens PRs to bump `VERSION` as they ship, - CI workflows under `.github/workflows/` that build on pull requests and upload to the SDK Store on push to the version branch, - a README template aligned to the rest of the project shape. Use it via GitHub’s “Use this template” button, or **git clone** if you don’t host on GitHub. The choice that follows is how to fill the template in. ### With the sdk-designer skill [template-sdk](https://github.com/canonical/template-sdk) also ships an agentic skill named `sdk-designer`. The skill runs an interactive scaffolding conversation: it asks about the software to package, the target platforms, and which interfaces and hooks are needed, then writes the corresponding files into the template. 1. Aim the agent at the new repository. 2. Run `/sdk-designer` and answer the prompts. 3. Review the generated files and adjust where the skill’s defaults don’t match your case. ### By editing the template directly Without the agentic skill, edit the template files in place. Fill `sdkcraft.yaml` per [Fill in the metadata](#how-build-sdk-metadata), replace each hook stub under `hooks/` per [Author the hooks](#how-build-sdk-hooks), update `VERSION` to the upstream release you intend to ship first, and adjust `renovate.json` if the upstream project lives somewhere other than the GitHub release page the default config targets. ## Fill in the metadata **SDKcraft** needs four pieces of metadata to identify and build the SDK: `name`, `version`, a one-line `summary`, and the `platforms` to build for. Add `license` to declare the SDK’s licensing terms, and `description` for a multi-line write-up: ```yaml name: version: "" summary: One-line description of the SDK description: | A longer description that explains what the SDK packages and any noteworthy behavior. license: MIT platforms: ubuntu@22.04:amd64: ubuntu@24.04:amd64: ``` Use the SDK’s upstream version for `version` when the SDK wraps a single tool; keep it quoted so that values like `1.0` aren’t parsed as floats. **SDKcraft** builds one artifact per entry in `platforms`. Each entry pairs an Ubuntu base with a CPU architecture from the Debian naming scheme (`amd64`, `arm64`, and so on). For SDKs that don’t ship compiled binaries, use `all` instead of a specific architecture. ## Define parts [Parts](https://ubuntu.com/workshop/docs//explanation/sdks/parts.md#exp-sdk-parts) describe how **SDKcraft** obtains the SDK’s payload at build time. A small SDK often gets by with a single part; larger SDKs split work along functional boundaries. For a binary downloaded from a release page, use the `dump` plugin with a tarball source: ```yaml parts: : plugin: dump source: https://example.com/releases/v${CRAFT_PROJECT_VERSION}/-linux-${CRAFT_ARCH_BUILD_FOR}.tar.gz source-type: tar ``` `$*CRAFT_PROJECT_VERSION*` and `$*CRAFT_ARCH_BUILD_FOR*` expand at build time from the `version` field and the platform **SDKcraft** is currently building for. If the SDK ships supporting files, add them as separate parts with the `file` source type: ```yaml parts: : plugin: dump source: https://example.com/releases/v${CRAFT_PROJECT_VERSION}/-linux-${CRAFT_ARCH_BUILD_FOR}.tar.gz source-type: tar service-unit: plugin: dump source: .service source-type: file ``` For source-built SDKs, the `rust`, `go`, and `python` plugins take over from `dump`. The Craft Parts [plugin reference](https://documentation.ubuntu.com/craft-parts/latest/reference/plugins/) lists every plugin and its options. ## Declare plugs and slots Plugs and slots wire the SDK to host resources and to other SDKs in the workshop. A plug requests access to something the workshop provides; a slot offers something the SDK exposes. The most common patterns are: - A `mount` plug for cache or model directories that should survive **workshop refresh**. - A `gpu` plug for SDKs that need GPU acceleration. - A `tunnel` slot for services that expose a network endpoint. For example, an SDK that runs a long-lived HTTP service and caches data under `~/.cache//` declares both a plug and a slot: ```yaml plugs: cache: interface: mount workshop-target: /home/workshop/.cache/ slots: api: interface: tunnel endpoint: 8080 ``` The `workshop-target` value is the in-workshop path that **Workshop** backs with persistent host storage; SDKs can’t pick the host path directly, which prevents them from reaching arbitrary host files. Workshop users can override the host side at run time with **workshop remount**. ## Author the hooks Hooks are the run-time logic of an SDK. **Workshop** runs them at specific lifecycle stages of the workshop they’re installed in. All hooks are shell scripts in `hooks/` and are linted with [ShellCheck](https://www.shellcheck.net/) when the SDK is packed. `SDK` is available inside every hook. It points to the SDK’s installation root inside the workshop; use it to reference files the SDK ships, for example `"$SDK/bin/"`. `SDK_STATE_DIR` is available only inside `save-state` and `restore-state`. It points to a temporary directory **Workshop** creates for one **workshop refresh** cycle: `save-state` writes to it before the old workshop is destroyed, and `restore-state` reads from it once the new workshop is up. The directory is gone as soon as the workshop stops, so don’t use it for long-lived data; back that with a `mount` plug or store it in the project directory instead. **Workshop** recognizes five hook names: `setup-base`, `setup-project`, `check-health`, `save-state`, and `restore-state`. A useful SDK rarely needs all five. ### setup-base `setup-base` runs as root when the SDK is first installed in a workshop and on every **workshop refresh**. It is the place for system-wide configuration: installing **apt** packages, wiring `PATH`, and laying down service unit files. A minimal `setup-base` for a single-binary SDK adds the SDK’s `bin/` directory to the system `PATH`: ```shell cat < /etc/profile.d/.sh export PATH="$SDK/bin:\$PATH" EOF ``` Place system-wide environment variables under `/etc/profile.d/` so they apply across shells. Avoid editing `/etc/bash.bashrc` directly; **Workshop** may support more than one shell and `/etc/profile.d/` is the portable seam. Inside `setup-base`, **apt** is preconfigured to skip recommended and suggested packages and to answer “yes” to confirmation prompts, so **apt-get install** calls can be terse: ```shell apt-get update apt-get install build-essential cmake ninja-build ``` Operations performed in `setup-base` become part of the workshop’s [base snapshot](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition-sdks), so subsequent refreshes start from a warmed-up state. ### setup-project `setup-project` runs as the `workshop` user after `setup-base`, once the project directory is mounted and interface plugs and slots are connected. This is the right place for per-user configuration: activating virtual environments, enabling user **systemd** services, and writing files under `/home/workshop/`. A typical `setup-project` for an SDK that ships a service unit installs and starts the service as a user-level systemd unit: ```shell install -D --mode=644 --target-directory ~/.config/systemd/user \ "$SDK/.service" systemctl --user daemon-reload systemctl --user enable --now ``` User-level **systemd** services are preferred over root-level ones because they cleanly tie their lifetime to the `workshop` user’s session and don’t require `sudo`. Operations in `setup-project` don’t go into the base snapshot, so use it for anything that depends on project-specific state or that should be re-evaluated on every launch. ### check-health `check-health` runs as root once every other hook has finished: on **workshop launch**, after `setup-project` has run for every SDK in the workshop; on **workshop refresh**, after `restore-state` has run for every SDK. **Workshop** also re-runs `check-health` on demand when it reassesses the workshop’s state. Use it to verify the SDK is functional and to report status back through **workshopctl set-health**. The canonical pattern is to exercise a real entry point and channel any error output back to the user: ```shell if ! output=$(sudo -u workshop --login --version 2>&1); then workshopctl set-health error "$output" exit fi workshopctl set-health okay ``` Run the command as `sudo -u workshop --login` so it picks up the same environment that a workshop user would see interactively; this catches PATH wiring bugs in `setup-base` that would otherwise stay hidden. Three health states are meaningful: - `okay`: The SDK is functional. - `error`: Something is wrong. Supply a message that helps a user understand what failed. - `waiting`: The hook should be retried. **Workshop** retries up to ten times, once per second. If the SDK never reaches `okay` or `error`, the health flips to `error` after those retries are exhausted. ### save-state and restore-state `save-state` and `restore-state` are an optional pair that only runs at **workshop refresh**. `save-state` runs in the old SDK revision, before **Workshop** destroys the old workshop. `restore-state` runs in the new SDK revision, after `setup-project` has finished for every SDK. Their job is to carry data across the refresh boundary in `SDK_STATE_DIR`. Because `restore-state` runs after `setup-project`, restored files aren’t yet present while `setup-project` is still executing; keep any setup that depends on restored state inside `restore-state` itself, or have `check-health` retry by reporting `waiting` until the state shows up. Use them when the SDK keeps configuration or transient data that doesn’t already live in a mount plug or a project file. Both hooks run as `root`, so reference the `workshop` user’s home explicitly rather than relying on `~`: ```shell if [ -d /home/workshop/.config/ ]; then cp -a /home/workshop/.config/ "$SDK_STATE_DIR/config" fi ``` ```shell if [ -d "$SDK_STATE_DIR/config" ]; then install -d -o workshop -g workshop /home/workshop/.config/ cp -fa "$SDK_STATE_DIR/config/." /home/workshop/.config// chown -R workshop:workshop /home/workshop/.config/ fi ``` Skip these hooks entirely when: - The SDK has no state worth preserving, for example a stateless CLI tool. - The state already lives in a directory backed by a `mount` plug, which survives refreshes by definition. - The state is regenerated cheaply by `setup-base` or `setup-project`. #### WARNING The SDK itself is refreshed as part of any **workshop refresh**. A bug in `save-state` or `restore-state` becomes a workshop-wide refresh failure, so test these hooks aggressively before relying on them. ## Try the SDK Once the definition and hooks are in place, build and install the SDK into a workshop with **sdkcraft try**: ```console $ sdkcraft try ``` **SDKcraft** packs the SDK for each declared platform into files of the form `__.sdk` and copies them into the [try area](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-test-try-sdk). Add the SDK to a workshop definition using the `try-` prefix: ```yaml name: dev base: ubuntu@24.04 sdks: - name: try- ``` The `base` must match one of the SDK’s `platforms`. Then launch the workshop with verbose output and a wait-on-error breakpoint so that any hook failure leaves a usable container behind for inspection: ```console $ workshop launch --verbose --wait-on-error ``` Pay particular attention to: - Hook output in **workshop changes** and **workshop tasks**. - The SDK’s `status` in **workshop info**; a `waiting` or `error` state is the SDK telling you something is wrong. - The interaction between this SDK and any other SDKs it’s meant to be installed alongside. On success, **workshop info** reports the SDK and a `status` of `okay`. On failure, **workshop changes** and **workshop tasks** point at the hook that failed; see [How to debug issues in workshops](https://ubuntu.com/workshop/docs//how-to/fix-workshops/debug-issues.md#how-debug-issues-workshops) for the full troubleshooting flow. ## Test the SDK If the SDK ships a `tests/` directory with [spread](https://github.com/canonical/spread) tests, run them against the freshly packed artifacts: ```console $ sdkcraft test ``` **SDKcraft** provisions a clean LXD container for each test, installs the packed SDK into a workshop, and runs the declared scenarios end-to-end. Tests live under `tests/`, organised in suites declared by `tests/spread.yaml`. The starter test at `tests/main/launch/` illustrates the layout; add more tests next to the starter, each in its own subdirectory of the same suite: ```yaml summary: SDK installs and reports healthy execute: | workshop launch --verbose --wait-on-error workshop info | grep -E 'status:\s+okay' ``` ## Iterate Normally, you would use the **workshop sketch-sdk** command to iterate on an SDK locally. However, even when it doesn’t fit your purpose, the build-try-fix loop is fast: 1. Edit the definition or a hook. 2. Run **sdkcraft clean && sdkcraft try** to rebuild from a clean state. 3. Run **workshop refresh** to reapply the SDK in the existing workshop, or **workshop launch --verbose --wait-on-error** for a fresh start. **sdkcraft clean** is optional; omit it when the change is small enough that **SDKcraft** can incrementally rebuild. For build internals, see the Craft Parts [lifecycle documentation](https://documentation.ubuntu.com/craft-parts/latest/common/craft-parts/explanation/lifecycle/). ## Next steps When the SDK behaves correctly under **sdkcraft try** and its test suite passes, proceed to [How to publish an SDK](https://ubuntu.com/workshop/docs//how-to/develop-sdks/publish-an-sdk.md#how-publish-sdk) to register the SDK name on the SDK Store and upload a revision. ## See also Explanation: - [Interface layout](https://ubuntu.com/workshop/docs//explanation/sdks/best-practices.md#exp-best-interfaces) - [Parts decomposition](https://ubuntu.com/workshop/docs//explanation/sdks/best-practices.md#exp-best-parts-decomposition) - [Parts or hooks?](https://ubuntu.com/workshop/docs//explanation/sdks/best-practices.md#exp-best-parts-or-hooks) - [Interfaces](https://ubuntu.com/workshop/docs//explanation/index.md#exp-interfaces) - [Design best practices](https://ubuntu.com/workshop/docs//explanation/sdks/best-practices.md#exp-sdk-best-practices) - [SDK concepts](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-concepts) - [SDK hooks](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks) - [SDK parts](https://ubuntu.com/workshop/docs//explanation/sdks/parts.md#exp-sdk-parts) - [Testing and trying SDKs](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-test-try-sdk) - [Using workshopctl with hooks](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-workshopctl) - [SDKs](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition-sdks) Reference: - [SDK definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdk-definition.md#ref-sdk-definition) - [SDK hooks](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-sdk-hooks) - [SDK parts](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-sdk-parts) - [workshop sketch-sdk](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-sketch-sdk) - [workshopctl (CLI)](https://ubuntu.com/workshop/docs//reference/cli/workshopctl.md#ref-workshopctl-cli) Tutorial: - [Craft SDKs with SDKcraft](https://ubuntu.com/workshop/docs//tutorial/part-4-craft-sdks.md#tut-craft-sdks) # camera-interface.md # Camera interface The camera interface enables access to the host system’s cameras and other video capture devices inside the workshop. By using the interface, the SDK publisher allows the workshop to access the host’s cameras, which can be useful in various SDK-specific tasks such as testing hardware or embedded devices. ## Camera interface plug An essential element here is the camera interface plug, which is declared in the SDK definition. Its structure includes just the name of the plug and the interface; both must be set to `camera`. Defining the plug in an SDK allows the workshops using this SDK to connect to the host’s cameras, which can be useful in various SDK-specific tasks such as testing hardware or embedded devices. ## Camera interface slot To let SDKs in a workshop access the host’s cameras, **Workshop** provides a camera interface slot that multiple camera interface plugs can access. When the SDK is installed at runtime during launch and refresh operations, **Workshop** checks that the plug targeting the slot passes [validation](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interfaces-validation); if it does, it can be connected. ## Connection The interface isn’t connected automatically at launch and refresh for security reasons. The **workshop connect** and **workshop disconnect** commands can be invoked manually after the workshop has started: ```console $ workshop connect ws/camera-sdk:camera $ workshop disconnect ws/camera-sdk:camera ``` Establishing a connection means that all existing `video4linux` and `media` devices will be made available inside the workshop. While the connection is active, adding new devices on the host will also make them available inside the workshop, whereas unplugged devices will also be removed from the workshop. To check if the interface is connected: ```console $ workshop connections --all INTERFACE PLUG SLOT NOTES ... camera ws/camera:camera ws/system:camera manual ``` This means the host’s cameras are available inside the workshop: ```console $ workshop shell ws workshop@ws-8584e571$ ls /dev/video* /dev/video0 /dev/video1 workshop@ws-8584e571$ ls /dev/media* /dev/media0 ``` ## See also Explanation: - [Interface concepts](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-concepts) - [Plugs and slots](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-plugs-slots) - [SDK definition](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-definition) - [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) Reference: - [workshop connect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connect) - [workshop connections](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connections) - [workshop disconnect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-disconnect) - [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) - [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh) - [workshop shell](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-shell) # camera.md # Camera interface The camera interface exposes a host camera device. - Plug attributes: none. - Plug name: must be `camera`. - Plug owner: any regular SDK; not the system SDK. - Slot: the system SDK provides a single `system:camera` slot. Other SDKs cannot declare camera slots. # changes-tasks.md # Changes, tasks A *change* is a core concept of the workshop state management system. Any long-running or invasive operation (e.g., [launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch)) that changes the state of a workshop is planned and applied as a change, which consists of specific tasks that run in a predefined order. A *task* is a small, independent piece of logic; it could be mounting a project directory, running a [hook](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks), or starting a workshop container. Most tasks are reversible. Overall, this scheme provides granular control over the state of a workshop; the state management system uses it to ensure the integrity of the workshop in the event of failure. By default, a failed change restores the workshop to its last operational state. To explicitly revert to the state after the last successful launch or refresh, use the **workshop restore** command. It restores the container filesystem from the most recent snapshot and resets all connections and mounts to their defaults. Unlike the automatic rollback during a failed refresh, **restore** is a deliberate user action to discard all changes made to a workshop since it was last set up. ## See also Explanation: - [SDK hooks](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks) How-to guides: - [How to debug issues in workshops](https://ubuntu.com/workshop/docs//how-to/fix-workshops/debug-issues.md#how-debug-issues-workshops) Reference: - [workshop changes](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-changes) - [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) - [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh) - [workshop restore](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-restore) - [workshop tasks](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-tasks) # cli.md # Command-line interfaces Of the four command-line interfaces provided by **Workshop**, **workshop** and **sdk** are aimed at **Workshop** users. Meanwhile, SDK authors who use **SDKcraft** will primarily interact with **sdkcraft** for building and publishing SDKs on the host, and **workshopctl** for SDK hooks to report state from inside a running workshop: * [sdk (CLI)](https://ubuntu.com/workshop/docs//reference/cli/sdk.md) * [sdkcraft (CLI)](https://ubuntu.com/workshop/docs//reference/cli/sdkcraft.md) * [workshop (CLI)](https://ubuntu.com/workshop/docs//reference/cli/workshop.md) * [workshopctl (CLI)](https://ubuntu.com/workshop/docs//reference/cli/workshopctl.md) # coding-style-guide.md # Workshop Go coding style guide This style guide documents Go-specific coding conventions used in the Workshop project. It captures patterns from code review discussions in merged PRs and established project standards. These guidelines complement Canonical’s general coding standards with Workshop-specific decisions. The guide is evidence-based, derived from actual PR discussions between maintainers during code reviews. --- ## Error handling **Error message format** **Pattern**: Error messages start lowercase, contain no trailing punctuation, and follow the template: “what was attempted: why it went wrong”. **Exception**: Proper nouns (like “SDK”, “LXD”) may start with a capital letter. **Rationale**: Maintains consistency with existing error handling patterns and provides clear, actionable user guidance. **Good**: ```go // From cmd/workshop/connect.go return fmt.Errorf("cannot connect plugs and slots across different workshops") // From cmd/workshop/list.go return fmt.Errorf(`cannot list: "--project" incompatible with "--global"`) // From internal/daemon/api_workshops.go return statusBadRequest("project-id required") ``` **Avoid**: ```go return fmt.Errorf("Cannot connect plugs.") // Starts with capital, has punctuation return fmt.Errorf("Error") // Not descriptive enough ``` --- **Quoting values in messages** **Pattern**: Interpolate dynamic values with `%q`. Wrap literal references (a fixed flag like `--global`, a command like `workshop list`, an enum keyword like `Ready`, an env var name like `SSH_AUTH_SOCK`, a JSON key, an attribute name) in double quotes inside a backtick raw string. Do not use single quotes for any kind of quoting in user-facing prose. Do not use `\"` escapes in double-quoted Go literals when the template contains literal double quotes; switch the delimiter to backticks instead. The same rules apply to Cobra `Short`/`Long`/`Example` strings and flag descriptions. **Good**: ```go // Dynamic value: %q quotes the interpolated identifier. return fmt.Errorf("workshop name %q too long", file.Name) // Literal flag names inside a template: backtick raw string with literal double quotes. return fmt.Errorf(`cannot list: "--project" incompatible with "--global"`) // Flag description with literal command references. cmd.PersistentFlags().BoolVar(&c.WaitOnError, "wait-on-error", false, `Pause the operation on error; to resume, use "--continue" or "--abort".`) // Wrapping an upstream error while quoting a literal config key. return fmt.Errorf(`internal error: cannot read "forget" flag: %w`, err) ``` **Avoid**: ```go // Escaped double quotes inside a double-quoted template. return fmt.Errorf("cannot list: \"--project\" incompatible with \"--global\"") // Single quotes around a literal keyword. return fmt.Errorf("%s must be a map or one of the shortcuts 'true' or 'false'", context) // Dynamic identifier rendered with %s instead of %q. return fmt.Errorf("workshop %s not found", name) ``` **Rationale**: `%q` is backed by `strconv.Quote`, the same primitive as `strutil.Quoted`, so dynamic values render consistently across error messages, log output, and list helpers. Reserving backticks as Go raw-string delimiters and double quotes for both literals and `%q` output keeps every quoted artifact in the final message visually uniform. The `docs/contributing.rst` Error messages guidance (path and identifier double-quoting) is the precedent. --- **Error specificity** **Pattern**: Return specific errors where possible to allow callers to handle them appropriately. **Good**: ```go if _, err := os.Stat(path); err != nil { if os.IsNotExist(err) { // Handle file not found specifically return fmt.Errorf("configuration file %q not found", path) } return fmt.Errorf("cannot access configuration file %q: %w", path, err) } ``` **Avoid**: ```go if _, err := os.Stat(path); err != nil { return fmt.Errorf("internal error") // Too generic, loses context } ``` **Rationale**: Specific errors enable proper error handling and debugging. Avoid generic “internal error” wrappers unless implementation details must be hidden. --- **Consistent error handling pattern** **Pattern**: Use one of two standard patterns consistently throughout the codebase. **For simple function calls**: ```go if err := f(); err != nil { return err } ``` **For functions with multiple returns**: ```go val, err := f() if err != nil { return err } ``` **Examples from codebase**: ```go // From cmd/workshopd/run.go if err := dirs.CreateDirs(); err != nil { return err } // From cmd/sdk/main.go if err := cmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } ``` **Rationale**: Consistent error handling improves code readability and maintainability. --- **Explicit error checking** **Pattern**: Always check and handle errors. Use `_ = someFunc()` to explicitly ignore intentionally. **Good**: ```go // Explicitly discarding error _ = file.Close() // Handling error if err := file.Close(); err != nil { log.Printf("failed to close file: %v", err) } ``` **Avoid**: ```go file.Close() // Unchecked error ``` **Rationale**: The errcheck linter is enabled for new code to catch unhandled errors. Note that errcheck ignores certain common cases like `file.Close()` and a few others. Explicit `_` assignment shows intentional discard for other cases. --- ## Naming conventions **Function names reflect behavior accurately** **Pattern**: Function names should accurately describe what the function does. Use the “maybe” prefix for operations that are conditional or may not occur. **Good**: ```go // Returns a value if conversion is possible, otherwise returns false func maybeFloatToInt(v float64) (int64, bool) { if _, frac := math.Modf(v); frac != 0 { return 0, false } return int64(v), true } // Conditionally presents warnings based on count and timestamp func maybePresentWarnings(count int, timestamp time.Time) { if count == 0 { return } // ... present warnings } // Returns SDK installation if the device represents one, otherwise nil func maybeSdkInstallation(key string, device map[string]string) (*workshop.SdkInstallation, error) { // Returns nil if device is not an SDK installation } ``` **Examples from codebase**: - `maybeRefresh()` - checks if refresh is needed - `maybeBound()` - returns binding if one exists - `maybePathError()` - wraps error as path error if applicable **Rationale**: The “maybe” prefix is an established pattern in the codebase indicating conditional behavior, optional operations, or operations that may not apply in all cases. --- **Descriptive variable names** **Pattern**: Use names that reflect purpose or filtering intent, not generic permission terms. **Good**: ```go filters := []string{ "config.user.workshop.project-id=" + pid, "config.user.workshop.name=" + w, "config.user.workshop.snapshot-type=" + kind, } snapshots, err := snapshotConn.GetInstancesWithFilter(api.InstanceTypeContainer, filters) ``` Here, `filters` clearly indicates these are filter conditions for querying snapshots via `GetInstancesWithFilter()`. **Avoid**: ```go conditions := []string{...} // Not aligned with the method name criteria := []string{...} // Too generic; criteria for what? ``` **Rationale**: Improves code clarity and communicates intent. Use names that describe what the variable represents, not what it enables. --- **Test constant naming** **Pattern**: Test constants should have sensible, descriptive names that reflect their purpose. **Good**: ```go const ( testProjectID = "test-project-123" testWorkshopName = "dev-workshop" fakeAPIResponse = `{"status": "ready"}` ) ``` **Avoid**: ```go const ( s1 = "test-project-123" ws = "dev-workshop" ) ``` **Rationale**: Improves test readability and maintainability. In tests, variable names can be more relaxed, but global, reusable test constants should still be descriptive. --- ## Code structure and organization **Complete related operations before moving to next attribute** **Pattern**: When processing multiple attributes, finish all operations related to one attribute before moving to the next. **Good**: ```go // Process target attribute completely target := plugAttrs["target"] if err := validatePath(target); err != nil { return err } parsedTarget := parsePath(target) // Now move to next attribute source := plugAttrs["source"] // ... process source ``` **Avoid**: ```go // Getting target target := plugAttrs["target"] // Getting source source := plugAttrs["source"] // Validating target (separated from getting it) if err := validatePath(target); err != nil { return err } ``` **Rationale**: Improves code clarity by maintaining logical grouping of operations. Related code stays together. --- **Extract common logic into reusable functions** **Pattern**: Extract duplicated completion logic into reusable functions rather than duplicating inline. **Good**: ```go func completeWorkshopName(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // Common workshop name completion logic return workshops, cobra.ShellCompDirectiveNoFileComp } cmd := &cobra.Command{ ValidArgsFunction: completeWorkshopName, } ``` **Avoid**: ```go cmd := &cobra.Command{ ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // Inline completion logic duplicated across commands cli, err := root.client() // ... 30 lines of logic ... return workshops, cobra.ShellCompDirectiveNoFileComp }, } ``` **Rationale**: Reduces duplication, avoids adding long inline functions into Cobra Command structure initialization, improves maintainability. --- **Prefer existing attributes over reiterating** **Pattern**: Use existing object attributes instead of reiterating collections when the information is already available. **Good**: ```go for _, plug := range plugs { if len(plug.Connections) > 0 { // Plug is connected } } ``` **Avoid**: ```go for _, plug := range plugs { isConnected := false for _, conn := range allConnections { if conn.Plug.Name == plug.Name { isConnected = true break } } } ``` **Rationale**: Improves readability and reduces unnecessary iterations when data is already present in objects. --- **Code should reflect logical flow clearly** **Pattern**: Structure code to match its logical intent — filter first, then transform. **Good**: ```go // Filter connected plugs var connectedPlugs []Plug for _, plug := range allPlugs { if len(plug.Connections) > 0 { connectedPlugs = append(connectedPlugs, plug) } } // Transform to suggestions for _, plug := range connectedPlugs { suggestions = append(suggestions, plug.ToCompletion()) } ``` **Avoid**: ```go // Mixed filtering and transformation for _, plug := range allPlugs { if len(plug.Connections) > 0 { suggestions = append(suggestions, plug.ToCompletion()) } } ``` **Rationale**: Makes intention explicit and improves readability for simple logic. --- ## Comments and documentation **Comment format** **Pattern**: Comments should be complete sentences starting with a capital letter and ending with a period. **Good**: ```go // Workshop represents a development environment running in a container. type Workshop struct { Name string Base string } // validateName checks that the workshop name is valid. func validateName(name string) error { // Empty names are not allowed. if name == "" { return fmt.Errorf("name cannot be empty") } return nil } ``` **Avoid**: ```go // workshop struct type Workshop struct { ... } // check name func validateName(name string) error { ... } ``` **Rationale**: Proper comment formatting improves readability and maintains professional documentation standards. --- **Godoc conventions** **Pattern**: Exported functions and types must have Godoc comments. The comment should start with the name of the element. **Good**: ```go // Workshop represents a development environment. type Workshop struct { ... } // Launch creates and starts a new workshop with the given configuration. func Launch(cfg *Config) (*Workshop, error) { ... } ``` **Avoid**: ```go // Represents a development environment. type Workshop struct { ... } // Creates and starts a workshop. func Launch(cfg *Config) (*Workshop, error) { ... } ``` **Rationale**: Following Godoc conventions ensures documentation is generated correctly and consistently. --- ## Type handling **Use type switches for multiple possible types** **Pattern**: When handling multiple possible input types, use type switches with explicit error messages for each case. **Good**: ```go switch ro := readOnly.(type) { case bool: return ro, nil case string: parsed, err := strconv.ParseBool(ro) if err != nil { return false, fmt.Errorf("invalid boolean string %q", ro) } return parsed, nil default: return false, fmt.Errorf("read-only must be bool or string, got %T", ro) } ``` **Avoid**: ```go if b, ok := readOnly.(bool); ok { return b, nil } if s, ok := readOnly.(string); ok { // ... parse string } // No clear error for other types ``` **Rationale**: Provides better error reporting and makes code more maintainable. This is the established pattern used throughout the codebase for handling multiple types. **Examples from codebase**: ```go // From internal/asserts/constraint.go switch x := v.(type) { case string: return x, nil case int: return strconv.Itoa(x), nil default: return "", fmt.Errorf("invalid type %T", v) } ``` --- **Avoid generics when concrete types are consistent** **Pattern**: Don’t use generics when type variation doesn’t actually exist. **Exception**: Test helpers and mock utilities may use generics to reduce code duplication across types. **Good**: ```go func filterByStatus(items []Workshop, status string) []Workshop { // Concrete types used consistently } ``` **Avoid**: ```go func filterByStatus[T any](items []T, status string) []T { // Unnecessary generics when T is always Workshop } ``` **Rationale**: Simplifies code when type variation doesn’t exist in practice. --- ## Testing patterns **Test with JSON response mocking, not ad-hoc interfaces** **Pattern**: In command packages, test API interactions by mocking JSON HTTP responses, not by creating ad-hoc client interfaces. **Good**: ```go // In cmd/workshop/connect_test.go func (s *connectSuite) TestConnect(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(ConnectionsResponse{ Plugs: []Plug{{Name: "test"}}, }) }) err := cmdConnect.Run(cmdConnect.Command(), []string{"test"}) c.Assert(err, check.IsNil) } ``` **Avoid**: ```go // Creating ad-hoc interface in cmd package type Client interface { Connections() ([]Connection, error) } func (c *CmdConnect) SetClient(cli Client) { c.client = cli } ``` **Rationale**: Keeps interface definitions in appropriate packages (client library) and maintains architectural boundaries. Command packages should focus on CLI logic, not define their own client interfaces. --- **Use real test data, not faked data** **Pattern**: Tests should use realistic data structures that match actual API responses. **Good**: ```go testWorkshop := Workshop{ Name: "dev", Base: "ubuntu@22.04", Status: "Ready", SDKs: []SDK{ {Name: "go", Channel: "latest/stable"}, }, } ``` **Avoid**: ```go // Overly simplified fake data testWorkshop := Workshop{ Name: "test", } ``` **Rationale**: Real data catches edge cases and integration issues that simplified fakes miss. --- **Minimize duplication in test setup** **Pattern**: Extract common test setup into helper functions or shared constants, but allow some duplication for clarity when needed. **Good**: ```go const readyWorkshopJSON = `{ "name": "dev", "status": "Ready" }` func (s *testSuite) setupReadyWorkshop(c *check.C) Workshop { return Workshop{Name: "dev", Status: "Ready"} } ``` **Avoid excessive coupling**: ```go // Reusing status across unrelated tests const sharedStatus = "Ready" // Used for both success and error cases ``` **Rationale**: Balance between DRY and test clarity. Some duplication is acceptable in tests to keep them self-contained and understandable. --- ## Architecture and separation of concerns **Sorting belongs in representation layer, not client library** **Pattern**: Client libraries should focus on data retrieval. Sorting and presentation logic belongs in the command/UI layer. Use the `slices` package for sorting. **Good**: ```go // In client library func (c *Client) Changes() ([]Change, error) { // Just retrieve and return data } // In cmd package func (c *CmdChanges) Run(cmd *cobra.Command, args []string) error { changes, err := c.client.Changes() if err != nil { return err } // Sort for presentation slices.SortFunc(changes, func(a, b Change) int { return cmp.Compare(b.ID, a.ID) }) } ``` **Avoid**: ```go // In client library func (c *Client) Changes() ([]Change, error) { changes, err := c.fetch() sort.Slice(changes, func(i, j int) bool { return changes[i].ID > changes[j].ID }) return changes, err } ``` **Rationale**: Separates data access from presentation concerns, making the client library reusable for different presentation needs. --- **CLI command patterns** **Pattern**: CLI commands should be transactional where possible and maintain consistent output formatting. **Transactionality**: ```go // Good: Use revert package for transactional operations import "github.com/canonical/workshop/internal/revert" func setupWorkshop(name string) error { r := revert.New() defer r.Fail() // Create container if err := createContainer(name); err != nil { return err } r.Add(func() { removeContainer(name) }) // Install SDKs if err := installSDKs(name); err != nil { return err // Automatically reverts container creation } r.Add(func() { uninstallSDKs(name) }) // Start workshop if err := startWorkshop(name); err != nil { return err // Automatically reverts everything } r.Success() // Mark as successful, skip revert return nil } ``` **Alternative: Manual defer cleanup**: ```go func setupMount(path string) (err error) { defer func() { if err != nil { // Clean up on error unmount(path) } }() if err := mount(path); err != nil { return err } if err := configure(path); err != nil { return err // defer will unmount } return nil } ``` **Help strings**: ```go // Good: Single spaces, concise Short: "Launch a new workshop", Long: `Launch creates and starts a workshop. The workshop will be based on the configuration in workshop.yaml.`, // Avoid: Multiple spaces or verbose explanations Short: "Launch a new workshop", Long: `This command will launch a new workshop. It will create the workshop based on the configuration...`, ``` **Output formatting**: ```go // Good: Use tabwriter for consistent table formatting import "text/tabwriter" func tabWriter() *tabwriter.Writer { return tabwriter.NewWriter(Stdout, 4, 3, 2, ' ', tabwriter.StripEscape) } func (c *CmdList) Run(cmd *cobra.Command, args []string) error { workshops, err := c.client.List() if err != nil { return err } w := tabWriter() fmt.Fprintf(w, "Name\tStatus\tBase\n") for _, ws := range workshops { fmt.Fprintf(w, "%s\t%s\t%s\n", ws.Name, ws.Status, ws.Base) } return w.Flush() } ``` **Rationale**: Transactional commands prevent partial failures from leaving the system in an inconsistent state. Consistent formatting and output improves user experience. --- ## Nil handling patterns **Use nil checks for “accept all” semantics** **Pattern**: Use nil to represent “accept all” filtering, and extract into named functions for clarity. **Good**: ```go matchesStatus := func(s string) bool { if status == nil { return true // nil means accept all } return slices.Contains(status, s) } for _, workshop := range workshops { if matchesStatus(workshop.Status) { results = append(results, workshop) } } ``` **Avoid**: ```go for _, workshop := range workshops { if status == nil || slices.Contains(status, workshop.Status) { // Inline logic less clear results = append(results, workshop) } } ``` **Rationale**: Makes the “accept all” intent explicit and improves readability. --- ## Code quality principles **Blank lines for logical separation** **Pattern**: Insert blank lines between logically different sections of code. **Good**: ```go func process() error { // Validation section if name == "" { return fmt.Errorf("name required") } if id == "" { return fmt.Errorf("id required") } // Data transformation section normalized := strings.ToLower(name) formatted := fmt.Sprintf("%s-%s", normalized, id) // Persistence section if err := save(formatted); err != nil { return err } return nil } ``` **Rationale**: Improves code structure and makes it easier to understand different logical sections. --- **Avoid nested conditions** **Pattern**: Use early returns to reduce nesting levels. **Good**: ```go func validate(workshop *Workshop) error { if workshop == nil { return fmt.Errorf("workshop is nil") } if workshop.Name == "" { return fmt.Errorf("name required") } if !isValidBase(workshop.Base) { return fmt.Errorf("invalid base") } return nil } ``` **Avoid**: ```go func validate(workshop *Workshop) error { if workshop != nil { if workshop.Name != "" { if isValidBase(workshop.Base) { return nil } else { return fmt.Errorf("invalid base") } } else { return fmt.Errorf("name required") } } else { return fmt.Errorf("workshop is nil") } } ``` **Rationale**: Reduces cognitive load, improves readability, and makes the happy path clearer. Use early returns or guard clauses to keep code flat and readable. --- **Delete dead code and redundant comments** **Pattern**: Remove unused code and comments that don’t add value. **Good**: ```go func process() error { // Handle special case for empty input if input == "" { return nil } return transform(input) } ``` **Avoid**: ```go func process() error { // TODO: implement this later // Legacy code from old implementation // input := getOldInput() // Get input input := getInput() // Check if empty if input == "" { // Return nil return nil } // Transform the input return transform(input) } ``` **Rationale**: Keeps codebase clean and maintainable. Redundant comments add noise without value. --- **Normalize symmetries** **Pattern**: Handle identical operations identically throughout the codebase. **Good**: ```go // Consistent error handling pattern everywhere if err := validateName(name); err != nil { return err } if err := validateBase(base); err != nil { return err } if err := validateSDKs(sdks); err != nil { return err } ``` **Avoid**: ```go // Inconsistent handling if err := validateName(name); err != nil { return err } err := validateBase(base) if err != nil { return err } if validateSDKs(sdks) != nil { return validateSDKs(sdks) // Called twice! } ``` **Rationale**: Consistency improves maintainability and reduces cognitive load when reading code. When the same operation appears in multiple places, it should be handled identically. --- ## Project-specific patterns **Cobra command structure** **Pattern**: Don’t inline long functions in cobra.Command initialization. Extract ValidArgsFunction and RunE implementations. **Good**: ```go func newCmdConnect() *cobra.Command { c := &CmdConnect{} cmd := &cobra.Command{ Use: "connect", RunE: c.Run, ValidArgsFunction: c.complete, } return cmd } func (c *CmdConnect) Run(cmd *cobra.Command, args []string) error { // Implementation } func (c *CmdConnect) complete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // Completion implementation } ``` **Avoid**: ```go func newCmdConnect() *cobra.Command { cmd := &cobra.Command{ Use: "connect", RunE: func(cmd *cobra.Command, args []string) error { // 50 lines of inline implementation }, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // 30 lines of inline completion logic }, } return cmd } ``` **Rationale**: Keeps command initialization clean and functions testable. --- ## Testing best practices **Integration test patterns** **Pattern**: Integration tests should test behavior, not internal implementation details. **Good**: ```go func (s *IntegrationSuite) TestLaunchWorkshop(c *check.C) { // Use c.Mkdir for automatic cleanup tmpDir := c.Mkdir() // Test the behavior err := s.cli.Launch("dev") c.Assert(err, check.IsNil) // Verify observable outcome workshops, err := s.cli.List() c.Assert(err, check.IsNil) c.Assert(workshops, check.HasLen, 1) c.Assert(workshops[0].Name, check.Equals, "dev") } ``` **Avoid**: ```go func (s *IntegrationSuite) TestLaunchWorkshop(c *check.C) { // Accessing internal state err := s.cli.Launch("dev") c.Assert(s.cli.internal.state.workshops["dev"].created, check.Equals, true) } ``` **Best practices**: - Use `c.Mkdir` from gocheck to create temporary directories that are automatically cleaned up - Avoid relying on internal implementation details - Test the behavior and observable outcomes - Source documentation examples in tests to ensure documentation stays in sync with code **Rationale**: Tests focused on behavior are more maintainable and less brittle when implementation changes. --- **Unit test patterns** **Pattern**: Use gocheck for unit tests, parameterize for edge cases, and avoid unnecessary mocks. **Good**: ```go func (s *ValidatorSuite) TestValidateName(c *check.C) { tests := []struct { name string input string expectedErr string }{ {"valid name", "dev-workshop", ""}, {"empty name", "", "name cannot be empty"}, {"invalid chars", "dev@workshop", "invalid character"}, } for _, tt := range tests { c.Logf("Testing: %s", tt.name) err := validateName(tt.input) if tt.expectedErr == "" { c.Assert(err, check.IsNil) } else { c.Assert(err, check.ErrorMatches, tt.expectedErr) } } } ``` **Best practices**: - Use gocheck for unit tests - Parameterize tests to cover edge cases (different URL formats, empty inputs, boundary conditions) - Avoid unnecessary mocks; prefer real lightweight implementations or fakes where feasible - Use real test data that matches actual API responses **Rationale**: Parameterized tests improve coverage, real data catches edge cases, and minimal mocking keeps tests maintainable. --- ## Internal package guidelines **Visibility control** **Pattern**: Keep types and functions unexported in `internal/` packages unless explicitly required by other packages. **Good**: ```go // internal/workshop/state.go // workshopState is internal to this package type workshopState struct { name string status string } // Workshop is exported for use by other packages type Workshop struct { Name string Status string } // internal helper func validateState(s *workshopState) error { ... } // Exported API func NewWorkshop(name string) (*Workshop, error) { ... } ``` **Avoid**: ```go // Everything exported unnecessarily type WorkshopState struct { ... } func ValidateState(s *WorkshopState) error { ... } ``` **Rationale**: Reduces API surface area, makes refactoring easier, and prevents unintended coupling between packages. --- **State management** **Pattern**: The state package provides two distinct mechanisms: `state.Get()`/`state.Set()` for persistent data, and `state.Cache()` for transient caching. **Persistent state with Get/Set**: ```go import "github.com/canonical/workshop/internal/overlord/state" // Store persistent data that survives restarts func saveConnectionState(st *state.State) error { conns := map[string]any{ "workshop/sdk:plug": "workshop/system:slot", } st.Set("conns", conns) return nil } // Retrieve persistent data func loadConnectionState(st *state.State) (map[string]any, error) { var conns map[string]any err := st.Get("conns", &conns) if err != nil && err != state.ErrNoState { return nil, err } return conns, nil } ``` **Transient caching with Cache**: ```go // Cache objects for quick access within a session (not persisted) func getStore(st *state.State) (*Store, error) { cached := st.Cached(cachedStoreKey{}) if cached != nil { return cached.(*Store), nil } store := newStore() st.Cache(cachedStoreKey{}, store) return store, nil } ``` **Avoid**: ```go // Don't do manual JSON serialization for state func saveState(path string, data any) error { json, _ := json.Marshal(data) return os.WriteFile(path, json, 0644) } ``` **Important considerations**: - `Get()`/`Set()` persist across restarts, serialized to JSON - `Cache()` is for session-only data, cleared on restart - Maps retrieved from state are references; modifications affect the original - Always lock state before Get/Set/Cache operations **Rationale**: State management APIs provide proper locking, change tracking, and persistence. Using them correctly avoids race conditions and ensures data consistency. --- ## Security considerations **Script injection prevention** **Pattern**: When generating scripts or templates, validate user input and use proper escaping mechanisms. **Good**: ```go import "github.com/canonical/workshop/internal/osutil" func generateSetupScript(userInput string) (string, error) { // Validate input first using whitelist approach if err := validateScriptInput(userInput); err != nil { return "", err } // Use proper escaping for mount paths escaped := osutil.Escape(userInput) script := fmt.Sprintf("#!/bin/bash\nmount %s\n", escaped) return script, nil } func validateScriptInput(input string) error { // Whitelist approach for allowed characters if !regexp.MustCompile(`^[a-zA-Z0-9_/-]+$`).MatchString(input) { return fmt.Errorf("invalid characters in input") } return nil } ``` **Available escaping utilities**: - `osutil.Escape()` - Escapes paths for mount entries - `osutil.Unescape()` - Unescapes mount entry paths - Input validation before any script generation **Avoid**: ```go func generateSetupScript(userInput string) string { // Direct interpolation without validation or escaping return fmt.Sprintf("#!/bin/bash\nmount %s\n", userInput) } ``` **Rationale**: Prevents script injection attacks when generating executable content from user input. Always validate and escape. --- **File permissions** **Pattern**: Be explicit about file permissions using appropriate constants or octal values. **Good**: ```go // Private file (owner read/write only) if err := os.WriteFile(path, data, 0600); err != nil { return err } // Public read, owner write if err := os.WriteFile(path, data, 0644); err != nil { return err } // Executable script if err := os.WriteFile(scriptPath, data, 0755); err != nil { return err } // Using constant for standard permissions if err := os.MkdirAll(dir, os.ModePerm); err != nil { // 0777 return err } // Standard file creation (rw-rw-rw-), relying on umask if err := os.WriteFile(path, data, 0666); err != nil { return err } ``` **Avoid**: ```go // Unclear permissions if err := os.WriteFile(path, data, 0777); err != nil { return err } // Magic numbers without context if err := os.WriteFile(path, data, 420); err != nil { // Decimal for 0644 return err } ``` **Rationale**: Explicit permissions ensure proper security boundaries and make intent clear. --- ## Common pitfalls and edge cases **Map initialization** **Pattern**: Always initialize maps before use. Writing to a nil map causes a panic. **Good**: ```go func newRegistry() *Registry { return &Registry{ workshops: make(map[string]*Workshop), sdks: make(map[string]*SDK), } } func addWorkshop(r *Registry, w *Workshop) { if r.workshops == nil { r.workshops = make(map[string]*Workshop) } r.workshops[w.Name] = w } ``` **Avoid**: ```go func addWorkshop(r *Registry, w *Workshop) { r.workshops[w.Name] = w // Panic if workshops is nil } ``` **Rationale**: Prevents runtime panics from nil map writes. --- **Loop variables in closures** **Pattern**: Be careful with loop variables in closures, especially in goroutines. **Good (Go 1.22+)**: ```go for _, workshop := range workshops { go func() { // Safe in Go 1.22+: each iteration has its own workshop variable process(workshop) }() } ``` **Good (Pre-Go 1.22 or explicit)**: ```go for _, workshop := range workshops { workshop := workshop // Create loop-local copy go func() { process(workshop) }() } ``` **Defer in loops** **Pattern**: Be careful when using `defer` inside loops. Defers execute at function exit, not loop iteration exit. **Good**: ```go func processFiles(files []string) error { for _, filename := range files { if err := processFile(filename); err != nil { return err } } return nil } func processFile(filename string) error { f, err := os.Open(filename) if err != nil { return err } defer f.Close() // Executes when processFile returns // Process file... return nil } ``` **Avoid**: ```go func processFiles(files []string) error { for _, filename := range files { f, err := os.Open(filename) if err != nil { return err } defer f.Close() // Won't execute until processFiles returns! // May accumulate many open files // Process file... } return nil } ``` **Rationale**: Defers in loops can cause resource leaks. Extract loop body into a function for proper cleanup. --- ## Contributing When contributing code: 1. Follow the patterns documented in this guide 2. Run `golangci-lint run` before submitting code 3. Write tests for new functionality 4. Use `go test ./...` to verify tests pass 5. Keep changes focused and atomic 6. Write clear commit messages For detailed contribution guidelines, see the [Contributing Guide](https://ubuntu.com/workshop/docs//contributing.md) in the documentation. # components.md # System components **Workshop** is designed to run on Linux systems and is primarily distributed as a [snap](https://snapcraft.io/). It relies on LXD as its containerization backend and ZFS for efficient storage management. **Workshop**’s distributed architecture organizes functionality across specialized subsystems, with each handling specific aspects of workshop lifecycle management while maintaining clear separation of concerns and well-defined communication interfaces. The core subsystems include the [workshopd daemon](#exp-arch-daemon) for orchestration, the [LXD backend](#exp-arch-lxd-backend) for container management, the [state database](#exp-arch-state-database) for transactional jobs, the [interface system](#exp-arch-interface-system) for resource sharing, and **systemd** integration for service lifecycle management. Storage is handled through the [ZFS storage component](#exp-arch-zfs-storage) with metadata persistence via the [state database](#exp-arch-state-database). #### NOTE For a description of how these components interact at runtime, see [Runtime behavior](https://ubuntu.com/workshop/docs//explanation/architecture/runtime-behavior.md#exp-arch-runtime-behavior). ## Main components **Workshop** installation on your host system includes three primary components: - The **workshop** [CLI](#exp-arch-cli) serves as the main user interface, providing a thin client that translates user commands and communicates with **workshopd** through a Unix domain socket. - The **workshopd** [daemon](#exp-arch-daemon) runs in the background with elevated privileges. It manages the full workshop lifecycle including container creation, SDK installation, interface coordination, and state persistence through a REST API. - The **workshopctl** [tool](#exp-arch-workshopctl) runs inside a workshop and has access to a very limited subset of **workshopd**’s REST API for service-related and reporting tasks. The communication architecture between these components uses a layered approach where the CLI communicates with **workshopd** via Unix domain sockets, while **workshopd** interfaces with LXD through the latter’s native API. ## CLI: **workshop** The **workshop** CLI provides a comprehensive command-line interface for managing workshops and interacting with **workshopd**. It is organized into logical command groups for different aspects of **Workshop**’s operations: - Create, update, and delete operations - Start and stop control - Exploration and troubleshooting - Interface connection management - Workshop utilization - SDK sketching - Miscellaneous utilities For all operations, the CLI communicates with **workshopd** through a REST API exposed over Unix domain sockets, using a client library that handles connection management, error handling, and request-response serialization. The client supports both synchronous and asynchronous operations. For further details, see [workshop (CLI)](https://ubuntu.com/workshop/docs//explanation/workshops/workshop-cli.md#exp-workshop-cli). ## Control interface: **workshopctl** The **workshopctl** tool serves as a secure bridge between workshop containers and the host system, sending control and status messages from inside the workshop back to the host. The tool is typically invoked by SDK hooks; it operates with the workshop user’s permissions (UID 1000) and communicates with **workshopd**’s [REST API](#exp-arch-api) through a dedicated Unix domain socket inside the workshop at `/var/lib/workshop/run/workshop.socket.untrusted`. The tool’s capabilities are intentionally limited to minimize security risks. ## Daemon: **workshopd** The **workshopd** daemon serves as the central orchestration hub, coordinating all workshop operations and maintaining system state. The daemon exposes the primary REST API for CLI and external integrations while orchestrating [workshop](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-concepts) lifecycle through transactional state management. It coordinates [interface connections](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-concepts) and policy validation. Key components within the daemon include: - the REST API server for handling HTTP requests, - the state engine as central coordinator, - the task runner for running tasks according to their dependencies, - and specialized state managers for workshops, [SDKs](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-concepts), [interfaces](#exp-arch-interface-system), commands, and hooks. During startup, the daemon initializes state managers, establishes an LXD connection, and starts the API server. It supports graceful shutdown with task completion and state persistence. Failure modes are handled through degraded mode operation for LXD unavailability and error detection for connection issues. The daemon provides **systemd** notification support and structured logging for telemetry. Authentication occurs via Unix domain socket credentials. Privilege separation exists between trusted and untrusted API endpoints. ### REST API The **workshopd** daemon exposes a versioned REST API (v1) over Unix domain sockets for secure local communication with the CLI. The API provides endpoints for all workshop operations. Trusted endpoints (require Unix socket credentials): - Workshop lifecycle operations (`/v1/projects//workshops`) - SDK management (`/v1/sdks`) - Interface connection management (`/v1/connections`) - Change tracking and monitoring (`/v1/changes`) - Warning and error reporting (`/v1/warnings`) Untrusted endpoints (accessible from within workshops): - Workshop control interface (`/v1/workshopctl`) The API implements proper access control and supports both synchronous and asynchronous operations. For data exchange, the API uses JSON with well-defined types for workshop information, SDK details, and interface connections. ### LXD backend The **workshopd** daemon maintains a persistent connection to LXD through its Unix domain socket at `/var/snap/lxd/common/lxd/unix.socket`, managing container operations. The LXD communication layer handles projects and container lifecycle, storage management, network configuration, and device pass-through. Its responsibilities also include base image management and caching, providing snapshot and restore capabilities for efficient workshop updates. **Workshop** implements user isolation through LXD’s project system, automatically creating dedicated projects for each user following the naming pattern `workshop.`. Each user project includes a corresponding snapshots project (`workshop-snapshots.`) used for temporary storage during workshop rebuild operations. #### NOTE The term “project” in relation to **Workshop** can be used in two unrelated senses: - LXD projects, identified by their names (e.g., `workshop.john`). These are created at **workshopd**’s request to provide isolation in LXD. - Workshop projects, identified by **Workshop**-assigned IDs. These are user-defined directories (e.g., `my-workshop`) used to organize and manage workshops (e.g., `my-workshop`). They are referenced in CLI commands and API endpoints. ### Storage backends **Workshop** uses ZFS for storage on Linux, with automatic Btrfs fallback on Windows Subsystem for Linux (WSL). Storage is managed via LXD and requires a minimum pool size of 5 GiB. The storage backend manages container root filesystems, workshop-specific data volumes, cached base images, and snapshots for efficient workshop updates and rollbacks. This component provides copy-on-write storage utilization, LZ4 compression, and quota management, and is utilized by the [LXD backend](#exp-arch-lxd-backend) for container operations. ### State database The `state.json` file is the authoritative database for workshop metadata, configuration, and operational state. It enables transactional operations with atomic updates and rollback capabilities. It uses a model of “changes” (high-level modification to the system state) and “tasks” (operations with Do/Undo handlers that constitute a change) to ensure that requests either complete fully or are rolled back. ### Images **Workshop** containers are created from base operating system images. By default, **Workshop** fetches base images, such as Ubuntu 22.04 LTS, Ubuntu 24.04 LTS, or Ubuntu 26.04 LTS, from the official [Ubuntu cloud image repository](https://cloud-images.ubuntu.com/releases/). ### Interfaces This system handles interface connections, enforces security policies, and also manages the lifecycle of resource connections. First of all, the interface system validates connections between plugs and slots. Built-in interface declarations enforcement handles auto- and manual connections. Key components include the interface repository serving as a registry of available interface types, policy validator for enforcing connection rules and security constraints, connection manager handling connection establishment and teardown, and security backends responsible for creating LXD profiles of established interface connections. For further details, see [Interface concepts](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-concepts). #### NOTE See [mount.go](https://github.com/canonical/workshop/blob/main/internal/interfaces/builtin/mount.go#L40) in **Workshop** source code for an elaborate example. ### Network **Workshop** establishes a dedicated network infrastructure through the `workshopbr0` bridge network, providing isolated networking for workshop containers. This bridge network includes DNS resolution configured with the `workshop` domain. ## Diagrams The system components and their interactions: # concepts.md # Workshop concepts A *workshop* (lowercase; not to be confused with **Workshop** itself) is a container that enables consistent environment builds. A workshop is defined by a single YAML file that acts as the blueprint for **Workshop** to implement at launch time. It describes how individual components fit together to create a cohesive development environment. A *project* is the working directory where workshop definitions are placed. When you start a workshop, the project directory is mounted inside it, so storing repositories, code, or data such as models in the project directory enables you to use them inside the workshop. Currently, these containers are hosted by [LXD](https://documentation.ubuntu.com/lxd/latest/), but it’s not recommended to rely on this implementation detail. ## Workshop status A workshop’s lifecycle can see it switch between several statuses: | State | Description | |-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | *Off* | Just defined, not operational;
the workshop container does not exist yet. | | *Ready* | Operational;
the workshop container is running and ready for use. | | *Stopped* | Operational;
the workshop container is stopped and can be restarted. | | *Pending* | Not operational;
the workshop container is running
but is being updated and is not ready for use. | | *Waiting* | Operational;
the workshop container is running and available for command execution,
typically for debugging a launch or refresh error;
the current [change](https://ubuntu.com/workshop/docs//explanation/workshops/changes-tasks.md#exp-changes-tasks) is in progress. | | *Error* | Not operational;
the workshop is in a nonfunctional state due to an error. | Status diagrams in the [See also]() section below provide more details of valid transitions. ## Launch, refresh, and restore Three commands move a workshop between the statuses listed above: - **workshop launch** builds a workshop for the first time - **workshop refresh** updates an existing workshop to match its current definition - **workshop restore** rolls a workshop back to the state it had right after its last successful launch or refresh. The table below summarizes when to use each command and what it does to the workshop and to its interface connections: | Command | Use case | Workshop effect | Connections effect | |----------------------|--------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------| | **workshop launch** | The workshop has never been built
and is in the *Off* state. | Builds the workshop from scratch,
installing the base image and each SDK in order. | All auto-connect candidates are evaluated
and connected for the first time. | | **workshop refresh** | The definition has changed
and you want those changes applied to the running workshop. | Reuses snapshots for SDKs whose configuration is unchanged;
reinstalls the rest from scratch. | Re-evaluates auto-connect against the new definition;
**any connections established manually after launch are dropped**. | | **workshop restore** | You want to discard runtime drift in the workshop
and return it to a known-good state. | Rolls the workshop filesystem back
to the snapshot taken at the last successful launch or refresh. | Re-evaluates auto-connect against the unchanged definition;
**any connections established manually since the snapshot are dropped**. | ### Launch First, **workshop launch** is the one-time builder. It applies the workshop definition layer by layer, taking a ZFS snapshot after each SDK so that later operations can reuse the work; see [SDKs](#exp-workshop-definition-sdks) for the layering details. Once a workshop has been launched, running **workshop launch** against it again fails with no effect. To apply changes from the definition to a launched workshop, use **workshop refresh**. ### Refresh Next, **workshop refresh** updates an existing workshop to match its current definition file. The workshop must be in the *Ready* state. Refresh is incremental: SDKs whose configuration is unchanged are kept as-is and restored from their snapshots without re-running their `setup-base` hook, while SDKs that have been **added, removed, or changed in the definition** go through a fresh installation. The `save-state` hook of each surviving SDK runs before the rebuild, and the matching `restore-state` hook runs after it, so SDK state kept by these hooks carries across the refresh. ### Restore Finally, **workshop restore** runs the same machinery as refresh, but uses the workshop’s state from the last successful launch or refresh as both the source and the target, ignoring any edits made to the workshop definition since then. The workshop must be in the *Ready* state. No SDK changes are applied; the container filesystem is rolled back to the snapshot it had right after the last successful launch or refresh, discarding any changes made inside the workshop since then. Restore is the right tool when the workshop has inadvertently drifted at runtime (for example, packages installed ad-hoc inside the container) and you want a clean slate without rebuilding from scratch. ## Workshop definition The workshop definition is a YAML file that lists the base image of the workshop and the specific components installed on top of it. It acts as a single source of truth about the workshop. It usually takes a few tries to produce a definition that works for your project, so you can edit and update the file iteratively. A simple workshop definition might look like this: ```yaml name: dev base: ubuntu@22.04 sdks: - name: go channel: "1.26" ``` It specifies a *base* and an *SDK*. A more complete definition would usually list several SDKs that use different [interfaces](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-concepts), software packages, and [hooks](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks). ## Base image The base specifies the underlying operating system image, such as a particular Ubuntu LTS release. This is the first layer of the workshop, upon which all other components are applied. For details on how the images are handled behind the scenes, see [Images](https://ubuntu.com/workshop/docs//explanation/architecture/components.md#exp-arch-images). ## SDKs The `sdks` section brings in the features and tools, layering them on top of the base image. Each SDK listed here is a bundle of code, data, and configurations, prepackaged with **SDKcraft** to be used with **Workshop**; see [SDK concepts](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-concepts) for details. This layering is not just conceptual; at launch time, **Workshop** uses ZFS snapshots to separate the SDKs: 1. The `base` OS is installed. 2. The `system` SDK is installed, and its `setup-base` hook is run. 3. A ZFS snapshot is taken, and cloned to create a new ZFS file system. 4. For each subsequent SDK in the order of their appearance on the `sdks` list, its `setup-base` hook is run and another snapshot is taken and cloned. This will create a chain of snapshots, where each one represents a cumulative layer of the workshop. Snapshots makes operations like refreshing or reverting a workshop very fast, as **Workshop** can simply restore a previous snapshot instead of rebuilding the environment from scratch. No snapshots are created for other hook types, such as `setup-project` or `save-state`. In order to restore an old snapshot, newer snapshots must be destroyed first. If refreshing fails, the workshop reverts to its previous state. The cloned file systems are used to restore the deleted snapshots. For details on how **Workshop** leverages ZFS, see [Storage backends](https://ubuntu.com/workshop/docs//explanation/architecture/components.md#exp-arch-zfs-storage). ## Plugs, slots, connections Once all the SDKs are installed, they often need to communicate with each other or with the host system. This is handled by establishing interface connections between plugs (service consumers) and slots (service providers); see [Interface concepts](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-concepts) for details. These plugs and slots can be defined in two ways: - By the SDK itself: An SDK can define its own plugs and slots in its `sdk.yaml` file. These are the standard capabilities the SDK offers. For Store SDKs, the `sdk.yaml` file is generated by **SDKcraft**; plugs and slots are copied as-is from `sdkcraft.yaml`. - Grafted by the workshop: A workshop definition can add plugs or slots to an SDK it references. This is done within an SDK’s entry in the `workshop.yaml` file. Grafting extends an SDK’s capabilities locally, possibly without the SDK publisher’s involvement or expectation; the user can add interface elements that the publisher didn’t anticipate, reducing the need for manual post-launch configuration. The `connections` section of the definition can explicitly link any plugs and slots available within the workshop, on top of what the [auto-connection mechanism](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-connections) in **Workshop** provides: eventually, all interface connections are [resolved, validated, and established](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interfaces-validation) in a single task *after* all the SDK layers have been created, because all components must be in place before the wiring can be done. This example adds a slot, a plug, and a connection to its SDKs: ```yaml base: ubuntu@22.04 name: dev sdks: - name: tensorflow plugs: cuda: interface: mount workshop-target: /usr/local/cuda/lib64 - name: imagenet slots: images: interface: mount workshop-source: $SDK/images - name: cuda connections: - plug: tensorflow:cuda slot: cuda:libs ``` This extends the `tensorflow` SDK with a standard path for CUDA runtime libraries. In `connections`, we explicitly connect the `cuda` plug, newly defined under the `tensorflow` SDK, to the `libs` slot from the `cuda` SDK. Thus, upon workshop creation, the plug will be connected not to a default system SDK location on the host (for example, `...///...`), but to a library path *inside* the workshop, which is set by `workshop-target`. Mind that the connection established in this way is no different from those created via the command line. ### Connections across refresh and restore Interface connections fall into three observable categories, each treated differently when a workshop is refreshed or restored: - *Auto-connections* are established at launch from the SDK’s auto-connect rules and from the `connections` section of the workshop definition. - *Manual runtime connections* are added with **workshop connect** after the workshop has been launched and are not present in the workshop definition. - *Manual disconnects of an auto-connection* are made with **workshop disconnect** against a connection that the workshop established by itself. The table below summarizes how each category is treated by **workshop refresh** and **workshop restore**: | Connection type and state | After **workshop refresh** | After **workshop restore** | |---------------------------------------------------------------------|------------------------------|-------------------------------------------| | Auto-connection still valid in the new definition | Re-established | Re-established | | Auto-connection whose plug or slot is removed in the new definition | Dropped | Not applicable; definition doesn’t change | | Manual runtime connection added with **workshop connect** | Dropped | Dropped | | Manually disconnected auto-connection | Stays disconnected | Stays disconnected | The practical consequence is that runtime use of **workshop connect** should be reserved for short-lived experimentation: to make a connection that survives a refresh, add it to the `connections` section of the workshop definition. Conversely, a deliberate **workshop disconnect** is preserved across refreshes and restores, so once a default auto-connection has been turned off, it stays off until explicitly reconnected. ## Actions Another optional part of a workshop definition is the `actions` section; it contains named shell scripts to be copied and executed inside the workshop. This section provides a degree of convenience, allowing the users to define simple aliases for longer or more complex shell commands that they expect to run frequently inside the workshop, right in the definition file. Because action bodies are **bash** scripts, they receive the trailing arguments of **workshop run** as standard positional parameters. Use `"$@"` to forward every argument and `"$1"`, `"$2"`, and so on to pick individual ones. Actions are not part of the layered snapshot system at all. They stay in the definition, and are parsed by the [daemon](https://ubuntu.com/workshop/docs//explanation/architecture/components.md#exp-arch-daemon) every time the **workshop run** command is executed. This means the users can add or modify actions and use them immediately, without needing to refresh or restart the workshop. The following example adds four actions, `lint`, `shellcheck`, `unit`, and `cover`, intended as utility helpers for a development environment: ```yaml name: dev base: ubuntu@24.04 sdks: - name: go channel: 1.26 actions: lint: | golangci-lint run --out-format=colored-line-number -c .golangci.yaml shellcheck: | git ls-files | file --mime-type -Nnf- | grep shellscript | cut -f1 -d: | xargs shellcheck --check-sourced --external-sources unit: | go test "$@" ./... cover: | go test ./... -coverprofile=coverage.out go tool cover -html=coverage.out ``` To run these actions, you use the **workshop run** command: ```console $ workshop run dev -- lint ``` When you thus invoke an action, it’s injected into the workshop and executed there in a fashion similar to **workshop exec**. Even if you update the `actions` section in the definition, there’s no need to refresh the workshop to use the updated action; it’s available immediately. For a quick reference of the actions in your workshop, run **workshop actions**: ```console $ workshop actions dev ``` This mechanism avoids the need to maintain helper scripts manually, ensuring instead that they are stored with the rest of the workshop’s metadata. ## Origins and locations Workshop components, including the many SDK types, originate from different sources and end up in multiple locations. The workshop definition file acts as a blueprint that brings these distributed components together: | Component | Origin | Storage location | Description | |------------------------------------------------------------------------------------------------------|------------------------------------------------------------------|---------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [Workshop definition](#exp-workshop-definition) | Created manually in YAML by **Workshop** users | Project directory on the host | Defines the workshop environment
and how it should be built and run. | | [System SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-system-sdk) | Built into **Workshop** | Automatically exposed in the workshop at launch | Provides host system integration capabilities
(mounts, camera, GPU, networking, and so on). | | [Regular SDKs](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-concepts) | Distributed via the SDK Store
with `channel` versioning | Downloaded, cached on the host,
and installed in the workshop at launch | These SDKs are the most common variation,
providing tools and libraries from external publishers. | | [In-project SDKs](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-in-project-sdk) | Created manually or ejected with **workshop sketch-sdk --eject** | Defined in the project directory on the host;
installed in the workshop at launch | Custom SDKs, specific to the workshop;
these are defined within the project directory
and can be identified by the `project-` prefix in their names
in the workshop definition. | | [Sketch SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sketch-sdk) | Generated with **workshop sketch-sdk** | Defined under `$XDG_DATA_HOME/workshop/`;
installed in the workshop at refresh | Encapsulates local, transient logic
in an SDK that can be quickly iterated upon
and later ejected as an in-project SDK. | | [Actions](#exp-workshop-definition-actions) | Defined by **Workshop** users | Listed directly in the workshop definition | Utility scripts, specific to the workshop;
these are injected into the workshop at run time. | ## See also Explanation: - [Interfaces](https://ubuntu.com/workshop/docs//explanation/index.md#exp-interfaces) - [Projects](https://ubuntu.com/workshop/docs//explanation/workshops/projects.md#exp-projects) - [SDKs](https://ubuntu.com/workshop/docs//explanation/index.md#exp-sdks) How-to guides: - [How to add actions to your workshop](https://ubuntu.com/workshop/docs//how-to/customize-workshops/add-actions.md#how-add-actions) - [Customize workshops](https://ubuntu.com/workshop/docs//how-to/index.md#how-use-workshops) Reference: - [workshop (CLI)](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-cli) - [workshop actions](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-actions) - [workshop connections](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connections) - [Workshop definition](https://ubuntu.com/workshop/docs//reference/definition-files/workshop-definition.md#ref-workshop-definition) - [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) - [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh) - [workshop restore](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-restore) - [workshop run](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-run) - [Workshop status diagrams](https://ubuntu.com/workshop/docs//reference/workshop-status.md#ref-workshop-status) # concepts.md # Interface concepts Interfaces are a mechanism for communication and resource sharing. It is an integral part of workshop confinement, ensuring that each workshop operates in its own isolated environment, while still allowing controlled interactions among the SDKs and with the host. In **Workshop**, SDKs can act as providers and consumers of resources such as the GPU, or file directories. Host system resources are exposed via the [system SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-system-sdk) that is present in every workshop by design. For a workshop to be operational, the SDKs listed in its definition must *connect* to the resources they expect. Such connections are uniformly established via the interface system. To achieve this, **Workshop** implements a counterpart to **snapd**’s [interface manager](https://snapcraft.io/docs/interface-management/), which controls whether an SDK can use resources beyond its confines. You can think of specific interfaces as resource *types*: filesystem, hardware, computing, and so on. Specific interfaces are predefined and implemented by **Workshop**, so you cannot create a custom interface type. Currently, **Workshop** and **SDKcraft** support the following: - [Camera interface](https://ubuntu.com/workshop/docs//explanation/interfaces/camera-interface.md#exp-camera-interface) (manually connected) - [Custom device interface](https://ubuntu.com/workshop/docs//explanation/interfaces/custom-device-interface.md#exp-custom-device-interface) (manually connected) - [Desktop interface](https://ubuntu.com/workshop/docs//explanation/interfaces/desktop-interface.md#exp-desktop-interface) (manually connected) - [GPU interface](https://ubuntu.com/workshop/docs//explanation/interfaces/gpu-interface.md#exp-gpu-interface) (auto-connected) - [Mount interface](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md#exp-mount-interface) (auto-connected) - [SSH interface](https://ubuntu.com/workshop/docs//explanation/interfaces/ssh-interface.md#exp-ssh-interface) (manually connected) ## Plugs and slots To make use of these interfaces, SDKs and [workshops](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition-connections) define *slots*. For example, a [mount interface](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md#exp-mount-interface) slot creates a source directory to be mounted inside the workshop via a plug. Further, SDKs and [workshops](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition-connections) define *plugs* to connect to a slot of a certain interface type. For example, a [mount interface](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md#exp-mount-interface) plug mounts the slot to a target directory inside the workshop. You can think of the plug as the recipient of the resources exposed by the slot; note that a slot can handle connections with multiple plugs. Connections can be established: - Automatically: By running **workshop launch**, **workshop refresh**, or **workshop start**. - Manually: By running **workshop connect** after the workshop has started, or by listing connections in the [workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition-connections) and running **workshop refresh**. All connections are subject to validation. Also, automatic connections require plugs and slots to have matching details and aren’t allowed for some interfaces, such as `ssh-agent`. Finally, the order of automatic connections is not guaranteed, so you should not rely on it. ## Validation All plugs and slots defined for a workshop directly or via its SDKs are checked to make sure they can be installed as part of the workshop and then connected. For this, **Workshop** uses a set of internal rules. Each interface has its own rule set; for example, the mount interface plug can be installed and auto-connected based on its rules alone. However, other interfaces may have different rules, such as allowing installation but not auto-connection for `ssh-agent`. ## Connections From the user perspective, connections can be established through the interface system in several ways: - The `connections` section of the workshop definition and the **workshop connect** command can be used to link interface plugs to respective slots, allowing the SDKs to orderly access the resources. - Conversely, the **workshop disconnect** command terminates existing interface connections, revoking the access to the resources granted by the connection. - Finally, the **workshop connections** command lists all existing connections and their states, providing an overview of how workshop connections are laid out. Some plugs can be auto-connected to their slots at launch or refresh. This behavior varies by interface, but the overall aim is to conduct reasonably in each case: the [mount](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md#exp-mount-interface) and the [GPU](https://ubuntu.com/workshop/docs//explanation/interfaces/gpu-interface.md#exp-gpu-interface) interfaces are auto-connected, whereas the [camera](https://ubuntu.com/workshop/docs//explanation/interfaces/camera-interface.md#exp-camera-interface), [custom device](https://ubuntu.com/workshop/docs//explanation/interfaces/custom-device-interface.md#exp-custom-device-interface), [desktop](https://ubuntu.com/workshop/docs//explanation/interfaces/desktop-interface.md#exp-desktop-interface), and [SSH](https://ubuntu.com/workshop/docs//explanation/interfaces/ssh-interface.md#exp-ssh-interface) interfaces require manual connection. Auto-connection also depends on where a plug or slot lives. Additional slots defined for the system SDK, for interfaces such as `tunnel` or `mount`, are not auto-connected at launch or refresh, largely for security reasons: the system SDK exposes sensitive host system resources. To the contrary, plugs added under the system SDK can be auto-connected, because they expose workshop internals. To know how each kind of connection is treated when a workshop is launched, refreshed, or restored, see [Connections across refresh and restore](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-connection-lifecycle). ## Plug bindings SDKs usually access host resources via [interface plugs](#exp-plugs-slots). When multiple SDKs try to use the same resource in conflicting ways, the workshop won’t launch and shows an error. To fix this issue, you can bind one plug to another of the same interface type. This makes both plugs point to the same resource without conflicts. Any action performed on one plug (like mounting or remounting) thus automatically applies to *all* bound plugs. When you run **workshop connections**, a bound plug will have `bind` listed under `Notes`, along with the line number of the target plug: ```console $ workshop connections digits INTERFACE PLUG SLOT NOTES mount digits/torchaudio:hub digits/system:mount bind.1 mount digits/torchvision:hub digits/system:mount bind.1 ``` Here, both plugs are listed as `bind.1`, pointing to `torchaudio:hub` in the *first* line. ## Related CLI operations A number of basic workshop operations affect plugs and slots in different ways. When you **workshop launch** a workshop, an auto-connect task handles each interface plug, finding a candidate slot, verifying the plug’s eligibility for the slot based on their declarations and connecting the two. On **workshop refresh**, existing connections are preserved in the refreshed workshop if their plugs were connected before the operation. A newer version of an SDK may drop a plug that was previously connected; such connections are removed, but the host-based content remains. On **workshop remove**, both the interface connections and the default host directories (if any have been created, for example, to accommodate mount interface slots) are removed. Also, you can manually enable or disable connections with **workshop connect** and **workshop disconnect**, whereas **workshop connections** can list all connections that have been established by any **Workshop** projects. #### NOTE We remove content stored in our default locations because it’s not a good idea to keep user data forever. Thus, at least some commands will delete this data to prevent it from piling up in hidden places where it’s unlikely to be used again. ## See also Explanation: - [SDKs](https://ubuntu.com/workshop/docs//explanation/index.md#exp-sdks) - [Workshops and projects](https://ubuntu.com/workshop/docs//explanation/index.md#exp-workshop) How-to guides: - [How to fix plug conflicts with binding](https://ubuntu.com/workshop/docs//how-to/fix-workshops/resolve-plug-conflicts.md#how-resolve-plug-conflicts) Reference: - [Command-line interfaces](https://ubuntu.com/workshop/docs//reference/index.md#ref-cli) - [workshop connect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connect) - [workshop connections](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connections) - [Workshop definition](https://ubuntu.com/workshop/docs//reference/definition-files/workshop-definition.md#ref-workshop-definition) - [workshop disconnect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-disconnect) - [workshop info](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-info) - [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) - [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh) - [workshop remove](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-remove) # concepts.md # SDK concepts With **SDKcraft**, you can package and publish software dependencies as isolated *SDKs* to be used in a workshop definition by **Workshop**, instead of managing them system-wide or through container images. SDKs encapsulate all required functionality, keeping installations clean and limiting access to system-level capabilities. Publishers handle installation and updates for SDKs, freeing users from maintaining complex image definitions or configurations. Most SDKs are designed by publishers and made available via the SDK Store, but some are specific to a particular project or user. A single workshop can include multiple SDKs from different sources. SDKs are distributed through channels similar to [snap channels](https://snapcraft.io/docs/channels/). ## SDK definition An SDK is described by two YAML files, one for building and one for installing: - `sdkcraft.yaml`, authored by the SDK publisher, is the *build-time* definition consumed by **SDKcraft**. See [SDKcraft project definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdkcraft-definition.md#ref-sdkcraft-definition). - `sdk.yaml`, generated by **SDKcraft** and embedded in the SDK package, is the *runtime* definition consumed by **Workshop** at install time. In-project SDKs skip the build step and author `sdk.yaml` directly. A workshop pulls SDKs together through its own `workshop.yaml`. A build-time definition looks like this: ```yaml name: go build-base: ubuntu@24.04 title: Go SDK summary: The Go programming language description: | Go is an open source programming language that enables the production of simple, efficient and reliable software at scale. version: "1.25.1" license: LGPL-2.1 platforms: amd64: build-on: [amd64] build-for: [amd64] arm64: build-on: [amd64] build-for: [arm64] riscv64: build-on: [amd64] build-for: [riscv64] plugs: mod-cache: interface: mount workshop-target: /home/workshop/go/pkg/mod parts: go: plugin: dump source: https://go.dev/dl/go$CRAFT_PROJECT_VERSION.linux-$CRAFT_ARCH_BUILD_FOR.tar.gz source-type: tar ``` ## SDK hooks **Workshop** and **SDKcraft** enable optional lifecycle *hooks* that control and extend the workshop’s internal behavior to make any framework wrapped as an SDK compatible with **Workshop**’s logic; in particular, the hooks manage the SDK state and report its health. Each hook is a shell script with domain-aware actions that **Workshop** runs in the workshop at a particular lifecycle stage to ensure that the SDK stays functional. Specific examples include `setup-base`, `setup-project`, `save-state` and `restore-state`. You may see individual hooks mentioned in the output of **workshop changes** and **workshop tasks**; understanding the events that trigger them can help you with troubleshooting. When you define an SDK, its hooks should be placed in the `hooks/` subdirectory next to the [definition](#exp-sdk-definition); **SDKcraft** lints them with [ShellCheck](https://www.shellcheck.net/) and packages them along with the `.yaml` file. ### Using **workshopctl** with hooks The **workshopctl** CLI tool allows an SDK to talk to the **workshopd** daemon. Under the hood, **workshopctl** uses a socket exposed by the daemon into the workshop environment. Overall, the interaction between SDKs and the **workshopd** daemon focuses on health checks in post-launch or refresh operations. ### SDK health, workshop status An SDK can report its health using the `workshopctl set-health` subcommand, which is typically invoked from the `check-health` hook when a workshop launches or refreshes. The command requires a health status. If it’s not `okay`, you can also supply an error code with a user-friendly message to provide further details. This command is essential for SDK publishers to communicate the health status of their SDKs within the workshop environment. Then, **workshopd** determines the overall [health status](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-status) of a workshop, such as *Ready*, *Pending* or *Error*; it depends on the runtime results of the `check-health` hook: - *Ready* means success: the hook set SDK health to `okay` and gracefully exited with a zero code. - *Pending*: The hook set the SDK health to `waiting`. This means it will be retried, one attempt per second. If the retries fail 10 times consecutively or if 5 seconds pass without `set-health` being invoked, the SDK health is changed to `error`. - *Error*: the hook exited with a nonzero code or explicitly set SDK health to `error`. ## SDK state An SDK can store any data specific to it, such as a model training configuration, within the workshop. To enable this, the SDK publisher implements save and restore [hooks](#exp-sdk-hooks) when building the SDK using **SDKcraft**. Later, **Workshop** runs these hooks at the appropriate moments to consistently handle such data, collectively known as *SDK state*. For example, before changes are applied to the workshop during **workshop refresh**, the states of the SDKs are saved by invoking their `save-state` hooks. On success, they are restored using the `restore-state` hooks. ## SDK platforms Platforms describe where SDKs can be built and installed. Some SDKs include compiled code, which only certain families of CPUs will understand. Others depend on particular versions of software provided by the workshop’s base image. The `platforms` section of the [definition](#exp-sdk-definition) lists the platforms that the SDK supports. Each build corresponds to one of these platforms. By default, **SDKcraft** builds SDKs for every possible platform. This typically means all platforms with the same CPU architecture as the build machine. When installing an SDK, **Workshop** will check its platform metadata for compatibility. **Workshop** and **SDKcraft** follow [Debian’s naming scheme](https://www.debian.org/ports/) for CPU architectures. SDKs that don’t ship compiled binaries can use the `all` architecture instead. ## System SDK Every workshop contains a special *system SDK* that exposes system resources through its slots. It cannot be installed from the SDK Store. Instead, it is automatically installed first during **workshop launch** and removed last during **workshop remove** to ensure internal consistency. The purpose of the system SDK isn’t to add hooks or additional content; it’s only there to uniformly expose host system resources to other SDKs. As such, it can’t be removed by the user. It’s also the only SDK that can have [mount interface](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md#exp-mount-interface) slots on the host. The uniformity of this approach lies in the fact that system resources and workshop resources are exposed using the same logic. You can also define additional plugs and slots for the system SDK, just as with any other SDK. ## Sketch SDK The sketch SDK is another special type of SDK. Like the system SDK, it’s unavailable from the SDK Store; instead, you define it inside the workshop using the **workshop sketch-sdk** command. Its purpose is to allow **Workshop** users to quickly make changes to a workshop beside the regular SDKs listed in the [definition](#exp-sdk-definition). Unlike a regular SDK, the sketch SDK: - doesn’t carry any persistent data - doesn’t appear on the definition - is unique to the workshop where it was created The sketch SDK can have [hooks](#exp-sdk-hooks) and use [interfaces](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-concepts), which allows it to interact with other SDKs. Note that `sketch` is a reserved name, and the sketch SDK is always installed last. ## Testing and trying SDKs Once an SDK is packed, publishers have two ways to exercise it before upload. The **sdkcraft test** command runs the SDK’s [spread](https://github.com/canonical/spread) tests against a freshly packed SDK. These tests live under the SDK’s `tests/` tree and are the publisher’s responsibility to author and maintain; **SDKcraft** only invokes the spread harness and reports results. The **sdkcraft try** command allows publishers to test SDKs before uploading them to the Store. Once installed in a workshop, these SDKs behave identically to SDKs from the Store. **SDKcraft** does not install SDKs in a workshop directly; it simply copies packed SDKs to a directory called the *try area*. **Workshop** looks in this directory when installing an SDK with the `try-` prefix. The try area has no channels; only one version of an SDK can be tested at a time. However, this version can be tested in multiple workshops with different [bases](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-base). ## In-project SDKs An *in-project SDK* resides within your project’s `.workshop/` directory. Unlike regular SDKs, which are published and distributed through the SDK Store, in-project SDKs are specific to your project and are version-controlled alongside your project’s source code. You can create an in-project SDK by ejecting a [sketch SDK](#exp-sketch-sdk) or by adding one manually, creating the appropriate directory structure with `sdk.yaml` and hooks. This approach allows you to customize the workshop to fit your project’s unique requirements, ensuring that all collaborators use the same environment and dependencies. They are a good fit when your SDK includes project-specific dependencies, tools, interface connections, or configurations that should remain private to the project and not be published or reused elsewhere. ## See also Explanation: - [Interfaces](https://ubuntu.com/workshop/docs//explanation/index.md#exp-interfaces) - [Projects](https://ubuntu.com/workshop/docs//explanation/workshops/projects.md#exp-projects) - [Workshops and projects](https://ubuntu.com/workshop/docs//explanation/index.md#exp-workshop) Reference: - [SDK definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdk-definition.md#ref-sdk-definition) - [SDK hooks](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-sdk-hooks) - [SDK internals](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-sdk-internals) - [SDKcraft project definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdkcraft-definition.md#ref-sdkcraft-definition) - [workshop changes](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-changes) - [workshop connect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connect) - [workshop connections](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connections) - [Workshop definition](https://ubuntu.com/workshop/docs//reference/definition-files/workshop-definition.md#ref-workshop-definition) - [workshop disconnect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-disconnect) - [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) - [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh) - [workshop start](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-start) - [workshop tasks](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-tasks) - [workshopctl (CLI)](https://ubuntu.com/workshop/docs//reference/cli/workshopctl.md#ref-workshopctl-cli) # connect-vscode.md # How to connect your local VS Code to a workshop A local VS Code instance can connect to a remote workshop environment via the `vscode-remote` SDK, giving you the full VS Code experience against **Workshop**. First, you’ll need to have the [remote development extension pack](https://code.visualstudio.com/docs/remote/remote-overview) installed. After that, add the `vscode-remote` SDK to your workshop definition: ```yaml name: dev base: ubuntu@24.04 sdks: - name: vscode-remote ``` Launch the workshop. Next, the output from **workshop tasks** will hint at the next steps: ```console $ workshop tasks ... VS Code → Open Remote Window → Connect to host → workshop@10.41.49.51 ``` Follow this guidance and type in the SSH address listed in the output (`workshop@10.41.49.51` in the sample above). In the terminal prompt, you’ll see that the IDE is running inside your workshop. #### NOTE If you’re having trouble finding the Connect to host command, mind that it’s enabled by the `Remote-SSH` extension from the extension pack mentioned above. ## See also Explanation: - [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) Reference: - [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) - [workshop tasks](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-tasks) # contributing.md # How to contribute We believe everyone has something valuable to contribute, whether you’re a coder, a writer, or a tester. Here’s how and why you could get involved: - **Why join us**: Work with like-minded people, grow your skills, connect with diverse professionals, and make a difference. - **What do you get**: Personal growth, recognition for your contributions, early access to new features, and the joy of seeing your work appreciated. - **Start early, start simple**: Dive into code contributions, improve documentation, or be among the first testers. Your presence matters, regardless of experience or the scale of your input. The guidelines below will keep your contributions effective and meaningful. ## Environment setup 1. `Workshop` has a client-server architecture. Its `workshopd` daemon exposes a RESTful API (see `internal/daemon/api.go`) to the clients. The recommended way to run the current sources is the `try` dev tool wired into `go.mod`: ```console go tool try ``` This builds `./cmd/...` into a temporary session directory under `try_sessions/`, starts `workshopd` against it, and drops you into a subshell with `WORKSHOP`, `WORKSHOP_CACHE`, `WORKSHOP_SOCKET` and `PATH` pre-configured. Exit the shell to tear the session down; pass `--keep` to retain the session directory for inspection. Re-run `go tool try` from inside the shell to rebuild and restart `workshopd` in place. If you’d rather drive `workshopd` directly: ```console go install ./cmd/... export WORKSHOP=~/workshop export WORKSHOP_CACHE=~/workshop-cache export WORKSHOP_DEBUG=1 workshopd run --create-dirs ``` The client can connect using the daemon’s Unix domain socket: ```console export WORKSHOP=~/workshop workshop list ``` 2. `Spread` is the end-to-end testing tool for `workshop`. Install it from [GitHub](https://github.com/canonical/spread): ```console git clone https://github.com/canonical/spread cd spread go install ./... ``` Make sure the `$GOPATH/bin` directory is included in `$PATH`. After successful installation, you should see the help message by running: ```console spread -h ``` To run the end-to-end test suite `tests/documentation/`, download the latest SDKcraft release from the [repository](https://github.com/canonical/sdkcraft/releases) and move it to the `tests/` directory. ## Coding In Workshop, commit messages differ from conventional commits in capitalization: ```none Ensure correct permissions and ownership for the content mounts * Work around an LXD issue regarding empty dirs: https://github.com/canonical/lxd/issues/12648 * Ensure the source directory is owned by the user running a workshop. Links: - ... - ... ``` The messages rarely, if ever, state the type of the commit (e.g., `fix`, `feat`, etc.); these are used for branch naming, for example: - `feat/workspace-start` - `fix/spread-tests-github` - `chore/update-lxd` Commits that focus on docs must use the `Doc:` type prefix with an optional scope in square brackets: ```none Doc[chore]: Align references ``` PR descriptions should follow the PR template checklist, which largely reiterates this section. After receiving review comments, optimize for commit history clarity. Address review comments with [fixup commits](https://git-scm.com/docs/git-commit/2.32.0#Documentation/git-commit.txt---fixupamendrewordltcommitgt) and rebase using [autosquash](https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---autosquash) when reasonable. ### Reversibility When making decisions that might be costly to reverse, explicitly state the rationale in the PR description. This helps to understand the reasoning and collaborate better. ### Coding standards - **Avoid nested conditions**: Refrain from nesting conditions to enhance readability and maintainability. - **Eliminate dead code and redundant comments**: Remove unused or obsolete code and comments. This promotes a cleaner code base and reduces confusion. - **Normalize symmetries**: Handle identical operations consistently, using a uniform approach. This also improves consistency and readability. ### Error handling When handling errors or multiple returns, follow a consistent pattern: ```go // one way to handle errors if err := f(); err != nil { ... } // one way to handle multiple returns val, err := f() if err != nil { ... } ``` ### Error messages - **Be consistent**: Try to match the style of existing error messages. Most of these can be found by searching for `fmt.Errorf` and `errors.New`. Paths and other identifiers should be double-quoted if possible. - **Quote values consistently**: Use `%q` for interpolated identifiers and `"..."` inside backtick raw strings for literal flag and command references. See [Workshop Go coding style guide](https://ubuntu.com/workshop/docs//coding-style-guide.md#coding-style-guide) for details. - **Consider the user experience**: Error messages should be clear and actionable. - **Be specific**: For example, if a file was not found, the error message should include its path. - **Mind the nesting**: Start in lowercase and avoid trailing punctuation. Avoid excessively long or repetitive error chains. A common template is: `what was attempted: why it went wrong`. ### Code structure - **Check coupled code elements**: Verify that coupled code elements, files and directories are adjacent. For instance, store test data close to the corresponding test code. - **Group variable declaration and initialization**: Declare and initialize variables together to improve code organization and readability. - **Divide large expressions**: Break down large expressions into smaller self-explanatory parts. Use multiple variables if necessary to make the code more understandable and choose names to reflect their purpose. - **Use blank lines for logical separation**: Insert a blank line between two logically distinct sections of code. This improves its structure and makes it easier to comprehend. ## Linting Code should be formatted consistently and avoid common pitfalls. Contributions will be checked for some of these issues using [golangci-lint](https://golangci-lint.run/). To run these checks locally: ```console golangci-lint run ``` Some issues can be fixed automatically: ```console golangci-lint run --fix ``` If [pre-commit](https://pre-commit.com/index.html#install) is available, `git` can run these checks on every commit: ```console pre-commit install ``` ## Testing Make sure to run unit and integration tests before submitting a PR. We use a `go test`-compatible [gocheck](https://pkg.go.dev/gopkg.in/check.v1#section-readme): ```console go test ./... go test -check.f ``` To run end-to-end tests and integration tests with [Spread](https://github.com/canonical/spread): ```console spread tests/ ``` To check code coverage: ```console go test -coverpkg=<./...|package> -covermode= -coverprofile= <./...|package> ``` For example, to measure coverage using all tests: ```console go test -covermode=count -coverpkg=./... -coverprofile=cover.out ./... ``` To generate an HTML representation: ```console go tool cover -html= -o ``` For example: ```console go tool cover -html=cover.out -o cover.html ``` The output flag can be omitted to open in the default browser: ```console go tool cover -html=cover.out ``` The above will work for unit and integration tests instrumented directly with go test. Integration tests run using spread will create the coverprofile automatically, however the artifacts will need to be collected from the VM. This can be accomplished by using the -artifacts flag when running spread. ```console spread -artifacts= tests/integration/ ``` ## Releases See the [release notes](https://ubuntu.com/workshop/docs//release-notes/index.md#release-notes) for more information on our general approach. The steps to produce a Workshop release are as follows. ### Build the snaps locally [Snapcraft](https://documentation.ubuntu.com/snapcraft/stable/) is used to build, package, and publish `workshop` snaps. All these processes run in a self-launched [LXD](https://documentation.ubuntu.com/lxd/latest/) container. To be able to run the build, install `snapcraft` and `lxd` using `snap`: ```console sudo snap install --classic snapcraft sudo snap install --channel=6/stable lxd ``` Add the current user to the `lxd` group to give permission to access its resources: ```console sudo usermod -a -G lxd $USER ``` Log out and reopen your user session for the new group to become active, then initialize LXD: ```console lxd init ``` ### Publish the release Here’s the publishing checklist to follow: - Merge and close the outstanding pull requests from the release scope - Make sure the unit, integration, and documentation tests are green; see [Testing]() for details - Update the documentation; see the [Release documentation]() section for the full checklist - Create and push a new release tag with `git`, using [semantic versioning](https://semver.org/) - Run the [release workflow](https://github.com/canonical/workshop/actions/workflows/release.yaml) on GitHub; this builds the release snaps for the supported architectures to be published on GitHub and adds a pull request to update the [CLI reference](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-cli) - Generate the [change log](https://github.com/canonical/workshop/releases/new) on GitHub ## Copilot configuration The repository includes configurations to help GitHub Copilot provide assistance; these are located in the `.github/` directory and include general instructions as well as customized agent prompts for specific tasks. ### Copilot instructions The `.github/copilot-instructions.md` file provides general project context to GitHub Copilot. Also, there are documentation- and code-specific instructions in `.github/docs.instructions.md` and `.github/go.instructions.md`, tailored to guide Copilot when assisting with documentation and Go code tasks, respectively. ### Custom agents The `.github/agents/` subdirectory contains [custom agent prompts](https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/customize-cloud-agent/create-custom-agents) for specific review and maintenance tasks: - `code-review.agent.md`: A code review specialist that enforces commit message standards, coding conventions, and error handling patterns, referencing this contribution guide and the [coding style guide](https://ubuntu.com/workshop/docs//coding-style-guide.md#coding-style-guide). - `doc-review.agent.md`: A technical documentation reviewer that performs a multistage review process including build validation, content analysis, and style checking, referencing this contribution guide and the [documentation style guide](https://ubuntu.com/workshop/docs//doc-style-guide.md#doc-style-guide). - `doc-schema-update.agent.md`: A specialized agent for reconciling the JSON schema in `docs/reference/definition-files/schema.json` with the validation logic in `internal/workshop/workshop_file.go`. These agents provide structured, actionable feedback and help maintain consistency across contributions. ## Documentation All documentation resides in the `docs/` directory. To build and run it at `127.0.0.1:8000`: ```console workshop launch workshop run docs-run ``` To suggest changes, submit a [pull request](https://github.com/canonical/workshop/pulls), limiting it to the `docs/` directory and prefixing the title with `Doc:`. ### Structure and style We use the [Canonical documentation starter pack](https://github.com/canonical/sphinx-stack) together with a custom Workshop in-project SDK in `.workshop/` to run and build our documentation; our preferred markup is reStructuredText (reST), with some opinionated style choices evident in the source. See the relevant documentation before making changes: - [Workshop documentation style guide](https://ubuntu.com/workshop/docs//doc-style-guide.md) (project-specific conventions and patterns) - [Starter pack](https://documentation.ubuntu.com/sphinx-stack/latest/) - [reST style guide](https://documentation.ubuntu.com/sphinx-stack/latest/reference/style-guide/) - [reST cheat sheet](https://documentation.ubuntu.com/sphinx-stack/latest/reference/doc-cheat-sheet/) ### Dependency management The documentation build requires Python 3.11 or later. Documentation dependencies are managed using `uv`: - `docs/requirements.in` contains dependencies specific to Workshop docs - `docs/requirements.txt` is the final, resolved dependency file The final file is generated by the `update-starter-pack` workflow, listed in [CI/CD](#contributing-cicd). ### CLI reference The [command-line reference](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-cli) for Workshop is produced directly from the Cobra command tree: ```console go run ./cmd/workshop generate-docs ``` The helper in `cmd/workshop/gendocs.go` uses the [Gencodo](https://github.com/canonical/gencodo) Go module to convert the command metadata into `.rst` files with clever templates. In particular, this is used during the [release workflow](#contributing-cicd). --- The [command-line reference](https://ubuntu.com/workshop/docs//reference/cli/sdkcraft.md#ref-sdkcraft-cli) for SDKcraft can be generated in the SDKcraft repository; run `gendocs.py` there to generate the files. Current implementation relies on [craft-application](https://github.com/canonical/craft-application/) and doesn’t fully integrate with Workshop documentation yet. ### Release documentation At every release, remember to: - Merge the auto-generated CLI reference pull request. - Bump the snap revision used across the docs. - Refresh the three schema files under `docs/reference/definition-files/`. Regenerate `schema-sdk.json` and `schema-sdkcraft.json` in a local SDKcraft repository checkout and copy the outputs over: ```console $ cd $ uv run python sdkcraft/models/metadata.py $ uv run python sdkcraft/models/project.py $ cp schema-sdk.json schema-sdkcraft.json \ /docs/reference/definition-files/ ``` The workshop `schema.json` is hand-audited against `internal/workshop/workshop_file.go`. - Update the [release notes](https://github.com/canonical/workshop/releases) with relevant details, following the established format; for an SDKcraft release, update the respective section in the same manner. - Copy the release notes to the documentation under `docs/release-notes/` and update the latest version in `docs/release-notes/index.rst`; the recent version lists should contain versions from the last 6 months. - Refresh the [coverage map](https://github.com/canonical/workshop/blob/main/docs/coverage.md) by running the `.github/workflows/doc-cover.yaml` workflow and merging the resulting pull request. - Copy the auto-generated SDKcraft CLI reference from the [SDKcraft repository](https://github.com/canonical/sdkcraft) to `docs/reference/cli/sdkcraft/`, making sure the updated documentation builds properly. ## CI/CD Multiple [GitHub Actions](https://docs.github.com/en/actions/get-started/understand-github-actions) workflows, defined in the `.github/workflows/` directory, automate testing, building, documentation, and release processes. Some of these workflows come from the [starter pack](#contributing-doc-structure) (marked SP), while others are custom-made for Workshop’s needs. Documentation workflows: | Workflow | Purpose | |--------------------------------------------------|-------------------------------------------------------------------| | `automatic-doc-checks.yml` (SP) | Build the documentation and fail on Sphinx warnings. | | `doc-cover.yaml` | Generate and update the documentation coverage map. | | `doc-update-sdk-schema.yml` | Update SDK schema files from the SDKcraft repository. | | `markdown-style-checks.yml` (SP) | Check style, spelling, and links in Markdown documentation files. | | `sphinx-python-dependency-build-checks.yml` (SP) | Ensure the Sphinx virtual environment can be built from source. | | `update-starter-pack.yaml` | Update documentation starter pack files weekly and on demand. | Code quality and testing workflows: | Workflow | Purpose | |-------------------|------------------------------------------------------------------------------------| | `cover.yaml` | Orchestrates `spread.yaml` and `unit-tests.yaml`;
aggregates coverage reports. | | `fixup.yaml` | Check for fixup and squash commits in pull requests. | | `lint.yaml` | Run `golangci-lint` on Go code. | | `scanning.yml` | Scan for known security vulnerabilities using Trivy. | | `spread.yaml` | Run end-to-end tests with Spread (reusable workflow). | | `unit-tests.yaml` | Run Go unit tests and check for race conditions (reusable workflow). | Build and release workflows: | Workflow | Purpose | |----------------------------|--------------------------------------------------------------------------------------------------| | `build-deps.yaml` | Build and cache Workshop snap (reusable workflow). | | `lxd-candidate-check.yaml` | Test Workshop against LXD candidate channel daily;
uses `build-deps.yaml`. | | `release.yaml` | Build release snaps for ARM64 and X64;
create GitHub release and trigger CLI docs update PR. | # custom-device-interface.md # Custom device interface The custom device interface enables access to arbitrary host devices inside the workshop, identified by the device *subsystem* they belong to (for example, `input`, `tty`, or `usb`). By using the interface, the SDK publisher allows the workshop to access devices that no dedicated interface covers, such as serial adapters, input devices, or other peripherals used for testing hardware or embedded devices. ## Custom device interface plug An essential element here is the custom device interface plug, which is declared in the SDK definition. Its structure includes the name of the plug, the interface (`custom-device`), and a `subsystem` attribute. Defining the plug in an SDK allows the workshops using this SDK to connect to matching devices, which can unlock additional SDK functionality. ## Device subsystems The `subsystem` attribute for a given device is defined by the Linux kernel. One way to query device properties is **udevadm info**: ```console $ udevadm info --query=property --property=SUBSYSTEM /dev/input/event0 SUBSYSTEM=input ``` ## Custom device interface slot To let SDKs in a workshop access the host’s devices, **Workshop** provides a custom device interface slot that multiple custom device interface plugs can access. When the SDK is installed at runtime during launch and refresh operations, **Workshop** checks that the plug targeting the slot passes [validation](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interfaces-validation); if it does, it can be connected. ## Connection The interface isn’t connected automatically at launch and refresh for security reasons. The **workshop connect** and **workshop disconnect** commands can be invoked manually after the workshop has started: ```console $ workshop connect ws/input-sdk:input-device :custom-device $ workshop disconnect ws/input-sdk:input-device ``` Establishing a connection means that all existing host devices belonging to the plug’s subsystem will be made available inside the workshop. While the connection is active, adding new devices on the host will also make them available inside the workshop, whereas unplugged devices will also be removed from the workshop. To check if the interface is connected: ```console $ workshop connections --all INTERFACE PLUG SLOT NOTES ... custom-device ws/input-sdk:input-device ws/system:custom-device manual ``` This means the host’s devices from the given subsystem are available inside the workshop: ```console $ workshop shell ws workshop@ws-8584e571$ ls /dev/input/ event0 event1 mice ``` ## See also Explanation: - [Interface concepts](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-concepts) - [Plugs and slots](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-plugs-slots) - [SDK definition](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-definition) - [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) Reference: - [workshop connect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connect) - [workshop connections](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connections) - [workshop disconnect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-disconnect) - [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) - [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh) - [workshop shell](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-shell) # custom-device.md # Custom device interface The custom device interface exposes host devices that belong to a Linux kernel subsystem. A custom device plug is described by this attribute: | Key | Value | Description | |------------------------|---------|-----------------------------------------------------------------------------------------------------| | `subsystem` (required) | string | The Linux kernel subsystem of the host devices to expose,
for example `input`, `tty`, or `usb`. | Plug owner: any regular SDK; not the system SDK. Slot: the system SDK provides a single `system:custom-device` slot. Other SDKs cannot declare custom device slots. # customize-workshops.md # How to use workshops Day-to-day **Workshop** usage covers a handful of recurring scenarios: adding actions, forwarding ports, moving projects, and running multiple workshops in parallel: * [Add actions to workshops](https://ubuntu.com/workshop/docs//how-to/customize-workshops/add-actions.md) * [Add mounts](https://ubuntu.com/workshop/docs//how-to/customize-workshops/add-mounts.md) * [Forward ports](https://ubuntu.com/workshop/docs//how-to/customize-workshops/forward-ports.md) * [Move projects around](https://ubuntu.com/workshop/docs//how-to/customize-workshops/move-projects.md) * [Use multiple workshops](https://ubuntu.com/workshop/docs//how-to/customize-workshops/use-multiple-workshops.md) # debug-issues.md # How to debug issues in workshops To trace the root cause of a workshop misbehaving at **workshop refresh** or any other action, you can explore its underlying changes and tasks, pause on error, list system-wide warnings, and acknowledge false positives. ## List tasks and changes Consider a workshop named `dev-volatile`, which uses an unstable SDK from the `latest/edge` channel: ```yaml name: dev-volatile base: ubuntu@22.04 sdks: - name: go channel: edge ``` Suppose something goes wrong during **workshop refresh**: ```console $ workshop refresh Error: cannot perform the following tasks: - Run hook "setup-base" for "go" SDK (command failed with an error code (1)) Refresh aborted ``` To show more details, try **workshop refresh --verbose**; you can also use `--verbose` with **workshop launch**. To see the *tasks*, or individual actions, during the latest *change*, which is essentially a major workshop update, run **workshop tasks** without arguments: ```console $ workshop tasks STATUS DURATION SUMMARY ... ``` If that didn’t help, investigate the failure further by listing all *changes* in the workshop to find the one that failed: ```console $ workshop changes ID STATUS SPAWN READY SUMMARY ... 81 Error today at 12:20 today at 12:21 Refresh workshops "dev-volatile" ``` When you have found the problematic change, list its *tasks* to see the cause, this time supplying the change ID as the argument: ```console $ workshop tasks 81 STATUS DURATION SUMMARY Done 59ms Retrieve "go" SDK from channel "latest/edge" Undone 42ms Create SDK state storage Done 28ms Run hook "save-state" for "go" SDK Done 31ms Disconnect interfaces of "go" SDK Done 29ms Disconnect interfaces of "system" SDK Undone 35ms Uninstall "go" SDK Undone 48ms Stash previous "dev-volatile" workshop Undone 52ms Restore "dev-volatile" workshop from "system" snapshot Undone 41ms Start "dev-volatile" workshop Undone 67ms Install "go" SDK Error 1m12.5s Run hook "setup-base" for "go" SDK Hold - Snapshot "go" SDK installation Hold - Mount project directory Hold - Resolve relations between interfaces of "dev-volatile" workshop Hold - Auto-connect interfaces of "go" SDK Hold - Run hook "setup-project" for "go" SDK Hold - Run hook "restore-state" for "go" SDK Hold - Run hook "check-health" for "go" SDK Hold - Remove SDK state storage Hold - Remove "dev-volatile" workshop from stash Done 24ms Remove "go" SDK profile ...................................................................... Run hook "save-state" for "go" SDK 2023-07-24T12:21:37 INFO GOBIN='/home/workshop/.local/bin' ...................................................................... Run hook "setup-base" for "go" SDK 2023-07-24T12:21:37 ERROR error: cannot install "go": cannot get nonce from store: persistent network 2023-07-24T12:21:37 ERROR error: Post "https://api.snapcraft.io/api/v1/snaps/auth/nonces": dial 2023-07-24T12:21:37 ERROR tcp: lookup api.snapcraft.io: Temporary failure in name resolution (exit code: 1) ``` The SDK-specific reason can be addressed individually. ## Wait on error The `--wait-on-error` option in **workshop refresh** and **workshop launch** pauses the refresh when an error occurs; instead of reverting the workshop to its previous state, **Workshop** will leave it as is for you to investigate: ```console $ workshop refresh --wait-on-error 2023-07-24T12:22:42+12:00 ERROR command exit code 1 error: cannot refresh; fix the errors reported, then run "workshop refresh --continue blank". To abort and revert, run "workshop refresh --abort blank" ``` To help determine what went wrong, use the **workshop changes** and **workshop tasks** commands discussed above. Next, you can shell into the workshop to debug and possibly fix it: ```console $ workshop shell ``` On success, you can resume the refresh process: ```console $ workshop refresh --continue ``` Otherwise, undo the changes with the `--abort` option: ```console $ workshop refresh --abort ``` The effect will be the same as if you hadn’t used `--wait-on-error`: the workshop will revert to its previous state. ## Isolate problematic SDKs When a workshop uses multiple SDKs and has issues during **workshop refresh** or **workshop launch**, it can be difficult to determine which SDK is causing the problem. Start by testing each SDK in isolation before combining them; this helps narrow down compatibility issues, integration problems, or SDK-specific bugs. If the workshop fails only when multiple SDKs are used together, the issue may stem from interactions between them. To isolate the culprit, comment out SDKs one by one in the workshop definition and refresh the workshop after each change. When the issue reappears, the cause is likely the SDK you just re-enabled, or its interaction with other SDKs. Investigate it using the **workshop tasks** command to view detailed error information. ## SDK-installed software versions Components installed via SDKs cannot be updated using their regular mechanisms. SDKs are mounted read-only inside workshops, so regular update commands won’t affect the SDK-provided files, likely failing instead. SDK definitions identify possible base systems and are usually versioned after the software they install; different SDK versions may be published via different channels. To update any components provided by an SDK, refresh the workshop with **workshop refresh**. This pulls the latest version of each SDK from its configured channel and installs the updated SDK components. To switch to a different version, update the channel in the workshop definition, then refresh the workshop to apply the change. ## List and suppress warnings **Workshop** occasionally encounters nonblocking or transient problems, such as broken mount points. These are registered as *warnings* in a system-wide log, which can be accessed with **workshop warnings**: ```console $ workshop warnings last-occurrence: 4 days ago, at 17:52 GMT warning: | dev-volatile/go:mod-cache mount is broken: /home/user/mod-cache does not exist ``` Multiple warnings about the same problem aren’t stacked; only their first and last occurrences are logged. You can suppress listed warnings with **workshop okay** to ignore them: ```console $ workshop okay ``` ## See also Explanation: - [Changes, tasks](https://ubuntu.com/workshop/docs//explanation/workshops/changes-tasks.md#exp-changes-tasks) - [SDKs](https://ubuntu.com/workshop/docs//explanation/index.md#exp-sdks) - [Workshops and projects](https://ubuntu.com/workshop/docs//explanation/index.md#exp-workshop) Reference: - [workshop changes](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-changes) - [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) - [workshop okay](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-okay) - [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh) - [workshop tasks](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-tasks) - [workshop warnings](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-warnings) # definition-files.md # SDK and workshop definition files Three YAML files describe what a workshop is and how the SDKs inside it behave. Each file has one authoritative reference page: - `workshop.yaml` is authored by workshop users; **Workshop** reads it to launch a workshop. See [Workshop definition](https://ubuntu.com/workshop/docs//reference/definition-files/workshop-definition.md#ref-workshop-definition). - `sdk.yaml` ships inside SDK packages; **Workshop** reads it at install time. Also authored directly for in-project SDKs. See [SDK definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdk-definition.md#ref-sdk-definition). - `sdkcraft.yaml` is authored by SDK publishers; **SDKcraft** reads it to build an SDK package, which embeds the generated `sdk.yaml`. See [SDKcraft project definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdkcraft-definition.md#ref-sdkcraft-definition). * [Workshop definition](https://ubuntu.com/workshop/docs//reference/definition-files/workshop-definition.md) * [SDK definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdk-definition.md) * [SDKcraft project definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdkcraft-definition.md) ## See also Explanation: - [In-project SDKs](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-in-project-sdk) - [SDK definition](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-definition) - [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) # desktop-interface.md # Desktop interface The desktop interface provides access to the host system’s display (Wayland/X11) socket(s) from inside the workshop, allowing it to securely run GUI applications. By using the interface, the SDK publisher allows the workshop to utilize the host’s display which can be useful for various SDK-specific tasks such as building graphical applications or using editors without remote support. ## Desktop interface plug An essential element here is the desktop interface plug, which is declared in the SDK definition. Its structure includes just the name of the plug and the interface; both must be set to `desktop`. Defining the plug in an SDK allows the workshops using this SDK to access the host’s display, which can be useful for various SDK-specific tasks such as building graphical applications or using editors without remote support. ## Desktop interface slot To let SDKs in a workshop access the host’s display, **Workshop** provides a desktop interface slot that multiple desktop interface plugs can access. When the SDK is installed at runtime during launch and refresh operations, **Workshop** checks that the plug targeting the slot passes [validation](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interfaces-validation); if it does, it can be connected. ## Connection The interface isn’t connected automatically at launch and refresh for security reasons. The **workshop connect** and **workshop disconnect** commands can be invoked manually after the workshop has started: ```console $ workshop connect ws/desktop-sdk:desktop $ workshop disconnect ws/desktop-sdk:desktop ``` Establishing a connection means a proxy Unix domain socket has been created and the following environment variables have been set: | Wayland | X11 | Both | |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `$WAYLAND_DISPLAY`


`$XDG_SESSION_TYPE`


`$QT_QPA_PLATFORM`


`$ELECTRON_OZONE_PLATFORM_HINT`


`$XDG_BACKEND`

| `$DISPLAY`


`$XDG_SESSION_TYPE`


`$QT_QPA_PLATFORM`


`$ELECTRON_OZONE_PLATFORM_HINT`


`$XDG_BACKEND`


`$XAUTHORITY`\*

| `$WAYLAND_DISPLAY`


`$DISPLAY`


`$XDG_SESSION_TYPE`


`$QT_QPA_PLATFORM`


`$ELECTRON_OZONE_PLATFORM_HINT`


`$XDG_BACKEND`


`$XAUTHORITY`\*

| *\*only set if present on the host* To check if the interface is connected: ```console $ workshop connections --all INTERFACE PLUG SLOT NOTES ... desktop ws/desktop-sdk:desktop ws/system:desktop manual ``` This means the host’s display socket (Wayland, X11 or both) is available inside the workshop: ```console $ workshop shell ws workshop@ws-8584e571$ ls $XDG_RUNTIME_DIR | grep wayland wayland-1 ``` ```console $ workshop shell ws workshop@ws-8584e571$ ls /tmp/.X11-unix X0 ``` ## See also Explanation: - [Interface concepts](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-concepts) - [Plugs and slots](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-plugs-slots) - [SDK definition](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-definition) - [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) Reference: - [workshop connect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connect) - [workshop connections](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connections) - [workshop disconnect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-disconnect) - [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) - [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh) - [workshop shell](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-shell) # desktop.md # Desktop interface The desktop interface exposes the host display server. - Plug attributes: none. - Plug name: must be `desktop`. - Plug owner: any regular SDK; not the system SDK. - Slot: the system SDK provides a single `system:desktop` slot. Other SDKs cannot declare desktop slots. # develop-sdks.md # How to develop SDKs These how-to guides cover the work of an SDK author: laying out the project, authoring hooks, and publishing the result to the SDK Store. * [Build an SDK](https://ubuntu.com/workshop/docs//how-to/develop-sdks/build-an-sdk.md) * [Publish an SDK](https://ubuntu.com/workshop/docs//how-to/develop-sdks/publish-an-sdk.md) # develop-with-workshops.md # How to develop with workshops **Workshop** integrates with developer tooling, from connecting your favourite IDE to running version control and CI/CD pipelines inside a workshop. ## Use IDEs and editors You can connect a locally installed IDE to a workshop over SSH, or run an editor or notebook environment directly inside your workshop and access it in your browser: * [Connect VS Code to a workshop](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/connect-vscode.md) * [Run JetBrains Gateway in a workshop](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/run-jetbrains-gateway.md) * [Run JupyterLab in your browser](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/run-jupyterlab-in-browser.md) ## Integrate with development workflows Workshops are intended to integrate with version control, CI/CD, and AI-powered development workflows: * [Manage Python environments](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/manage-python-environments.md) * [Run GitHub Actions locally](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/run-github-actions-locally.md) * [Run workshops in GitHub Actions](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/run-workshops-in-github-actions.md) * [Use workshops with AI agents](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/use-workshops-with-ai-agents.md) * [Use workshops with Git](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/use-git.md) # doc-style-guide.md # Workshop documentation style guide This style guide documents the established conventions used in the Workshop documentation. It captures actual patterns observed across the documentation set and serves as a reference for maintaining consistency in new contributions. This guide is subordinate to Canonical’s documentation standards but records Workshop-specific decisions and patterns that extend or clarify those standards. --- ## File naming and organization **Directory structure** The documentation follows the [Diátaxis](https://diataxis.fr/) framework with four main sections: ```default docs/ ├── tutorial/ # Step-by-step learning paths ├── how-to/ # Task-oriented guides ├── explanation/ # Conceptual information └── reference/ # Technical specifications ``` **File naming convention** All filenames use lowercase letters and dashes for word separation. Examples: - Good: `part-1-get-started.rst` - Good: `connect-vscode.rst` - Good: `camera-interface.rst` - Good: `sdk-vs-dockerfile.rst` - Avoid: `ConnectVSCode.rst` (uppercase) - Avoid: `camera_interface.rst` (underscore) Tutorial files use a sequential numbering pattern: ```default part-1-get-started.rst part-2-work-with-interfaces.rst part-3-sketch-sdks.rst part-4-craft-sdks.rst ``` How-to files: Use verb-first naming pattern: ```default add-actions.rst connect-vscode.rst forward-ports.rst debug-issues.rst resolve-plug-conflicts.rst ``` Explanation files use noun-based naming: ```default concepts.rst camera-interface.rst best-practices.rst runtime-behavior.rst ``` Reference files match command structure: ```default workshop-launch.rst workshop-connect.rst sdkcraft-build.md ``` Filenames and directory names in the documentation repo should be in lowercase, with dashes instead of spaces; the directory tree must be built in a way that provides for readable, meaningful URLs: `/docs/howto/change-tyres`. --- ## Page structure and metadata **Standard page structure** Every reStructuredText documentation page follows this structure: ```restructuredtext .. _anchor_label: .. meta:: :description: Brief description for search engines and social media Page Title ========== Opening paragraph providing context and purpose. Section Heading --------------- Content... Subsection Heading ~~~~~~~~~~~~~~~~~~ Content... ``` **Metadata block** Every page must have a `.. meta::` block immediately after the anchor label. Format: ```restructuredtext .. meta:: :description: A brief, clear description of the page content for SEO and social media. Typically 1-2 lines, wrapping at natural phrase boundaries. ``` Examples from the documentation: ```restructuredtext .. meta:: :description: Practical introduction to workshops, guiding users through defining, launching, and refreshing workshops, and executing commands in workshops. ``` ```restructuredtext .. meta:: :description: A comprehensive explanation of the Workshop interface system, detailing how SDKs connect to host system resources through interfaces, and the mechanism of plugs and slots for resource sharing between containers. ``` **Anchor labels** Use lowercase with underscores, prefixed by section type. Prefixes: - `tut_` - Tutorial sections - `how_` - How-to guides - `exp_` - Explanation articles - `ref_` - Reference documentation Examples: ```restructuredtext .. _tut_get_started: .. _how_add_actions: .. _exp_interface_concepts: .. _ref_workshop_launch: ``` **Artefact comments** Use `.. @artefact` comments to mark key concepts for coverage tracking: ```restructuredtext .. @artefact workshop (container) .. @artefact SDK .. @artefact interface .. @artefact workshop launch ``` The current list of these concepts is maintained in `docs/coverage.yaml`; update it as needed. --- ## Writing style and tone **Voice and audience** Target audience is developers and DevOps professionals seeking to: * Achieve specific goals without much overhead and roundabout musings * Perform and conceive complex ad-hoc tasks and workflows that require precision and depth * Attain understanding of Workshop’s key capabilities beneficial for their scenarios Content follows the Diátaxis framework, providing: * Concise tutorials for common, starter-level actions and scenarios, eliminating the need to invent custom steps and allowing novice users to journey along the hot path effortlessly * Elaborate explanations of the thinking behind Workshop’s design, including design decisions, related concepts, and how it should be used * Detailed how-to guides that address specific needs of advanced users and cover topics beyond basic entry-level operations * Comprehensive reference of all options, settings, and details available to customize Workshop’s operation in any desirable manner The tone is authoritative but relaxed, confident but approachable. Think water cooler conversation, not classroom session. Example from the documentation: ```text Workshop is a tool for defining and handling ephemeral development environments. List your dependencies and components in YAML to define an environment. The key pieces of a definition are SDKs, independent but connectable units of functionality created by software publishers and available on the SDK Store. Workshop simplifies experiments with your environment layout. ``` **Direct instructions** Use imperative mood for instructions. Avoid “you can” or “you may” for required actions. Preferred: ```default Install Workshop using the --classic option: ``` Avoid: ```default You can install Workshop with: ``` **Paragraph length** Keep paragraphs focused and relatively short (2-5 sentences typically). Complex topics should be broken into multiple paragraphs. Example from tutorial: ```restructuredtext Install Workshop, upgrading the prerequisites if needed, then ensure it runs. Authenticate to the Snap Store and install the snap using the `--classic <...>`_ option: ``` **Clarity over cleverness** - State prerequisites explicitly - Define terms at first use - Avoid assumptions about reader knowledge - Use precise, unambiguous language **Language and spelling** Convention: Use US English spelling, grammar, and formatting conventions throughout the documentation. Examples: - Good: `color`, `center`, `analyze`, `behavior` - Avoid: `colour`, `centre`, `analyse`, `behaviour` - Good: Use serial comma: “SDKs, interfaces, and workshops” - Good: Double quotes for quotations: “Workshop is a tool” --- ## Semantic line breaks **Pattern** The documentation consistently uses semantic line breaks (one line per clause or significant phrase) in reStructuredText files. This improves version control diffs and editing precision. Rationale: Semantic breaks make git diffs more readable and help reviewers identify exactly what changed in a sentence or paragraph. **Implementation** Break lines at natural semantic boundaries: - After each complete clause - Before coordinating conjunctions (and, but, or) - Before relative clauses (which, that, who) - After introductory phrases Example from the documentation: ```restructuredtext This is the first section of the :ref:`four-part series `; a practical introduction that takes you on a tour of the essential |ws_markup| activities. ``` ```restructuredtext To make use of these interfaces, SDKs and :ref:`workshops ` define *slots*. For example, a :ref:`mount interface ` slot creates a source directory to be mounted inside the workshop via a plug. ``` ```restructuredtext When crafting SDKs for |ws_markup|, publishers face design decisions that affect how their SDKs install, integrate, and work inside workshops. Understanding the best practices outlined below helps publishers create more maintainable, reliable, and user-friendly SDKs that better align with |ws_markup|'s architecture and ideology. ``` **When to break** Break after: - Complete independent clauses - Introductory prepositional phrases - Transitional phrases - Items in a complex series Keep together: - Short phrases that form a single unit - Inline markup and its target word - Cross-reference markup Example: ```restructuredtext Interfaces are a mechanism for communication and resource sharing. It is an integral part of workshop confinement, ensuring that each workshop operates in its own isolated environment, while still allowing controlled interactions among the SDKs and with the host. ``` --- ## Headings and titles **Capitalization** Pattern: Sentence case for all headings (capitalize only first word and proper nouns). Examples: ```restructuredtext Get started with workshops ========================== Install |ws_markup| ------------------- Prerequisites ~~~~~~~~~~~~~ ``` Exception: Product names and proper nouns maintain their capitalization: ```restructuredtext How to use JetBrains Gateway with Workshop ========================================== ``` **Heading hierarchy** reStructuredText heading levels (consistent across documentation): ```restructuredtext Page Title (H1) =============== Section (H2) ------------ Subsection (H3) ~~~~~~~~~~~~~~~ Sub-subsection (H4) ^^^^^^^^^^^^^^^^^^^ ``` ### How-to title pattern How-to guides follow the pattern: “How to [action] [object]” Examples: ```default ^^^^^^^^^^^^^^^^^^^^ ``` **How-to title pattern** How-to guides follow the pattern: “How to [action] [object]”: - How to forward ports with tunneling - How to fix plug conflicts with binding - How to debug issues in workshops Linking exception: In navigation and links, drop “How to” prefix and use infinitive: ```restructuredtext How-to guides: * Debug issues in workshops * Connect VS Code to a workshop ``` --- ## reStructuredText conventions **Code blocks** Standard format: ```restructuredtext .. code-block:: console $ workshop launch dev ``` ```restructuredtext .. code-block:: yaml :caption: workshop.yaml name: dev base: ubuntu@22.04 ``` With emphasis: ```restructuredtext .. code-block:: yaml :caption: workshop.yaml :emphasize-lines: 7-9 name: dev base: ubuntu@22.04 sdks: - name: go channel: 1.26 actions: lint: | golangci-lint run ``` Supported languages: `console`, `yaml`, `python`, `go`, `shell`, `ini`, `json` **Admonitions** Note: ```restructuredtext .. note:: For other ways to install LXD, see the available installation options in `LXD documentation <...>`_. ``` Warning: ```restructuredtext .. warning:: This will permanently delete all workshop data. ``` **Placement:** Place admonitions at the end of the subsection they relate to, rather than interrupting the flow of text in the middle of a section. **Inline markup** Semantic markup preference: Use semantic markup roles (`:samp:`, `:envvar:`, `:file:`, etc.) instead of generic ones (\`, \*, etc.). Choose the most specific role that suits the purpose and use it consistently. Emphasis (italics): ```restructuredtext A *workshop* is a development environment running in a container. ``` Use italics sparingly to introduce new terms (a link is even better) and for emphasis. Leave bold for product names and commands. Strong (bold): Rarely used; prefer other markup when possible. Program/command names: ```restructuredtext :program:`workshop` :command:`workshop launch` ``` Commands in `:command:` roles should be presented in their complete form (e.g. `workshop launch`, not just `launch`) and should not be used as verbs or nouns in the text. Use non-breaking spaces in inline command literals to prevent longer compound commands from wrapping. File paths: ```restructuredtext :file:`workshop.yaml` :file:`/home/user/.ollama/models/` ``` End directory path names with a slash where possible and conventional to disambiguate directories from files. Sample values: ```restructuredtext :samp:`ollama` :samp:`ssh-agent` ``` Environment variables: ```restructuredtext :envvar:`PATH` :envvar:`HOME` ``` Placeholders: Format placeholders in uppercase within angle brackets, without underscores: ```restructuredtext :samp:`workshop launch {WORKSHOP}` :samp:`{SDK-NAME}@{CHANNEL}` ``` Or in documentation text: ```default workshop launch ``` Substitutions are reusable text replacements defined in `docs/reuse/substitutions.txt` and automatically included in all reStructuredText files: ```restructuredtext |ws_markup| # Renders as :program:`Workshop` |sdk_markup| # Renders as :program:`SDKcraft` ``` These ensure consistent formatting of product names throughout the documentation. Use them instead of typing product names manually. Common external links are defined in `docs/reuse/links.txt` for consistent reference across documentation: ```restructuredtext .. _Canonical website: https://canonical.com/ .. _GitHub: https://github.com/canonical/workshop/ .. _LXD: https://documentation.ubuntu.com/lxd/latest/ .. _SDKcraft: https://github.com/canonical/sdkcraft/ .. _Releases: https://github.com/canonical/workshop/releases/ ``` Reference these with trailing underscores: ```restructuredtext See the `GitHub`_ repository for source code. Refer to the `LXD`_ documentation for setup details. ``` **Non-breaking spaces:** Use non-breaking spaces (U+00A0 or `~` in LaTeX contexts) for important proper names and inline compound commands where line breaks would be awkward, though this is rarely needed in reStructuredText. **Lists** Bulleted lists: ```restructuredtext - Camera interface (manually connected) - Desktop interface (manually connected) - GPU interface (auto-connected) ``` Numbered lists: Use pound signs for auto-numbering: ```restructuredtext #. First step #. Second step #. Third step ``` Multiline list items: Separate items with a blank line for visibility if at least one item is multiline: ```restructuredtext - First item with a longer description that spans multiple lines - Second item that is also long and needs proper spacing - Third item ``` **Table of contents** Follow this pattern, avoiding hidden ToCs where possible: ```restructuredtext Heading ======= Some summary of what's to follow. These articles say this and this: .. toctree:: :glob: :maxdepth: 1 * These articles say this and this: .. toctree:: :glob: :maxdepth: 1 * ``` **“See also” sections** “See also” sections can appear on pages under any pillar and link to related content not immediately essential but potentially useful. Break link lists down by pillar, listing pillars and individual subsections in alphabetical order: ```restructuredtext See also -------- Explanation: * :ref:`exp_sdks` * :ref:`exp_workshop` How-to guides: * :ref:`how_resolve_plug_conflicts` Reference: * :ref:`ref_cli` * :ref:`ref_workshop_connect` * :ref:`ref_workshop_info` ``` Or using custom link text: ```restructuredtext See also -------- Explanation: * :ref:`changes, tasks (concepts) ` * :ref:`project (concept) ` * :ref:`workshop (concept) `, :ref:`workshop definition (file) ` Reference: * :ref:`workshop changes (command) ` ``` Links listed here must point to concepts used in the main document; vice versa, concepts appearing in the main text should be linked in “See also” if a well-established related page exists. Special case: If “See also” is the only subsection on the page, hide the sidebar ToC on the right using the `:hide-toc:` directive at the top of the file. **Tab headings** Pattern: Keep tab headings noun-based and consistent across related content. Avoid “sticky toggling” (where tab state persists inappropriately across different contexts). Example: ```restructuredtext .. tabs:: .. tab:: Ubuntu Installation instructions for Ubuntu... .. tab:: macOS Installation instructions for macOS... ``` **Rubric directive** Used in CLI reference for section headers: ```restructuredtext .. rubric:: Usage .. code-block:: console $ workshop launch ... [flags] .. rubric:: Description This command constructs the workshops... .. rubric:: Examples Launch the 'nimble' and 'jazzy' workshops: ``` **Sphinx extensions and roles** Preference: Use Sphinx-specific [roles](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html) and [directives](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html) over `docutils` generic equivalents. Use all their options and capabilities, listing options in alphabetical order. Example with options: ```restructuredtext .. code-block:: yaml :caption: workshop.yaml :emphasize-lines: 3-5 :linenos: name: dev base: ubuntu@22.04 sdks: - name: go channel: 1.26 ``` **Spacing and formatting** Use three spaces to indent reStructuredText directives and separate directive content with a blank line: ```restructuredtext .. tab:: .. code-block:: console $ workshop launch dev ``` Separate `list-table` rows with blank lines: ```restructuredtext .. list-table:: * - **Tutorial** - :ref:`Get started ` • ... * - **Workshops** - :ref:`Concepts ` • ... ``` Section gaps: Include a noncumulative two-line gap (two blank lines) after code samples, lists, tables, and before headings for visual clarity. Examples from the documentation: After code blocks: ```restructuredtext .. code-block:: console $ sudo snap install --classic workshop Prerequisites ~~~~~~~~~~~~~ ``` After lists: ```restructuredtext - :command:`workshop stop` doesn't destroy the workshop, unlike :ref:`remove ` - :command:`workshop start` doesn't build it from scratch, unlike :ref:`launch ` or :ref:`refresh ` In the next step, you'll refresh an existing workshop. ``` After tables: ```restructuredtext .. list-table:: :header-rows: 1 :widths: 25 75 * - Component Type - Description * - Runtime components - Core binaries and libraries that change infrequently However, parts are not mandatory: ``` Before headings: ```restructuredtext The actions you're about to perform cover most of your daily needs with |ws_markup|. .. _tut_install: Install |ws_markup| ------------------- ``` --- ## Markdown style conventions This section covers Markdown-specific conventions used in the Workshop documentation. For general writing style, see the “Writing style and tone” section above. ### When to use Markdown Markdown is used for: - Release notes (`release-notes/v0.*.md`) - Auto-generated CLI reference (`reference/cli/sdkcraft/*.md`) - Special files rendered on GitHub (`security.md`, `coverage.md`) For all other documentation, prefer reStructuredText (`.rst` files). ### File naming Use the version number as the filename for release notes: `vX.Y.Z.md`. ### Metadata blocks Pattern: Markdown files should include metadata using the `{eval-rst}` directive at the top of the file. Required for: - Release notes (`release-notes/v0.*.md`) - Any Markdown documentation files that will be rendered in Sphinx Format: ```markdown ```{eval-rst} .. meta:: :description: Brief description for search engines and social media. ``` # Page Title ``` Exception: Auto-generated CLI reference files for SDKcraft (`reference/cli/sdkcraft/*.md`) do not require metadata blocks, as they are automatically generated from command definitions. Example from release notes: ```markdown ```{eval-rst} .. meta:: :description: Release notes for Workshop v0.1.28, highlighting key changes, new features, and bug fixes in this version. ``` # Workshop v0.1.28 release notes ``` ### Headings Use ATX-style headers (`#`) with sentence case (capitalize only first word and proper nouns): ```markdown # Page title ## Section heading ### Subsection heading ``` ### Links Use inline link syntax: ```markdown See [Privately reporting a security vulnerability](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability) for instructions. ``` Avoid reference-style links in Markdown files. ### Code blocks Use fenced code blocks with language specifiers: ```markdown ```console workshop launch dev ``` ```yaml name: dev base: ubuntu@22.04 ``` ``` ### Lists Use hyphens (`-`) for unordered lists: ```markdown - First item - Second item - Third item ``` Use numbers for ordered lists: ```markdown 1. First step 2. Second step 3. Third step ``` ### Emphasis - *Italics*: Use single asterisks `*text*` - **Bold**: Use double asterisks `**text**` ### Release notes template Use the following template for new release notes, ensuring all links and version numbers are updated: ```markdown ```{eval-rst} .. meta:: :description: Release notes for Workshop vX.Y.Z, highlighting [key features]. ``` # Workshop vX.Y.Z release notes ## [Day] [Month] [Year] These release notes cover new features and changes in Workshop vX.Y.Z. ## Requirements and compatibility Workshop relies on Snap and LXD: - See the [Tutorial](https://ubuntu.com/workshop/docs/tutorial/) for setup instructions. - Refer to the [Contribution Guide](https://ubuntu.com/workshop/docs/contributing/) for development prerequisites. ## What's new in Workshop vX.Y.Z [Brief summary of the release]. ### [Feature Name] [Description of the feature and its benefit]. ---- **Full Changelog**: https://github.com/canonical/workshop/compare/vX.Y.Z-1...vX.Y.Z ``` ### Simplified markup for GitHub-rendered files Use simplified markup for files that have special meaning on GitHub and need to be rendered there (such as `docs/readme.rst` or `docs/contributring.rst`). These files should be more accessible to users viewing them directly. Key simplifications: - No `$` prompts in code blocks: GitHub doesn’t prevent prompt selection during copying, which can confuse users. - No semantic roles: Fall back to generic markup instead (e.g., use double backticks for code instead of `:command:` or `:program:`). - Plain link syntax: Use simple inline links instead of reference-style links. Examples: ```restructuredtext .. code-block:: console sudo snap install --classic workshop ``` Notice: No `$` prompt, making it easier to copy commands directly. Notice: Plain inline links instead of reference-style links, generic double backticks instead of semantic roles. --- ## Code examples **Console examples** Pattern: Show command with prompt, followed by output (if relevant): ```restructuredtext .. code-block:: console $ workshop launch dev Launching dev... Launched dev ``` Command prompts: Use the nonselectable `$` prompt. The `console` lexer in `.. code-block::` automatically handles this, making the prompt nonselectable during copy operations. Root access: When root access is required, include `sudo` explicitly: ```restructuredtext .. code-block:: console $ sudo snap install workshop --classic ``` Command output: Indent output with two spaces and separate it from the command with a blank line: ```restructuredtext .. code-block:: console $ workshop list --global PROJECT WORKSHOP STATUS NOTES ~/myproject dev Running - ~/otherproject test Stopped - ``` Comments in commands: Use two forms for comments: ```restructuredtext .. code-block:: console # Full line comment explaining the command $ workshop launch dev $ workshop exec dev -- echo "test" # Inline comment with two spaces before # ``` **Configuration examples** Always include caption when it can be deduced from context: ```restructuredtext .. code-block:: yaml :caption: workshop.yaml name: dev base: ubuntu@22.04 sdks: - name: go channel: 1.26 ``` Indentation: Use commonly recognized formatting: - YAML files: 2-space indentation - JSON files: 4-space indentation **Multi-line shell commands** Use backslash continuation or explicit line breaks: ```restructuredtext .. code-block:: console $ workshop connect dev/ollama:host 127.0.0.1:11434 \ --host-port 11434 ``` --- ## Cross-references and links **Internal cross-references** Preferred method: Use `:ref:` links with semantic labels, not paths: ```restructuredtext :ref:`tut_get_started` :ref:`how_add_actions` :ref:`exp_interface_concepts` ``` With custom text: ```restructuredtext :ref:`four-part series ` :ref:`workshop definition ` ``` Avoid `:doc:` links: Use `:doc:` links sparingly and only in specific contexts where finer manual control over table of contents lists is needed. Currently acceptable uses: - Home page (`index.rst`) for primary navigation structure - Release notes (`release-notes/index.rst`) for version listings For all other internal documentation links, prefer `:ref:` with semantic anchor labels, as they are more robust to file reorganization and provide better error checking. **External links** Inline: ```restructuredtext `LXD documentation `_ ``` Anonymous: ```restructuredtext See the `Snapcraft guide `__ for details. ``` **Link text guidelines** Avoid: Generic “click here” or “see this” text Prefer: Descriptive phrases integrated into the sentence Example: Good: ```default See the available installation options in LXD documentation. ``` Avoid: ```default See here for more details. ``` **First mention pattern** Link important terms only at first mention on a page. Avoid excessive linking. **Reference label convention** Use the following underline convention for `:ref:` anchor labels: ```restructuredtext .. _ref_workshop_launch: .. _how_add_actions: .. _exp_interface_concepts: .. _tut_get_started: ``` Pattern: `.. _{prefix}_{descriptive_name}:` where prefix indicates the section type (ref/how/exp/tut). --- ## Terminology, product names **Product names** Workshop - Always capitalized, never “workshop” when referring to the product. SDKcraft - Always use capital SDK, never “Sdkcraft” or “sdkcraft”. LXD - Always uppercase. **Technical terms** workshop (lowercase) - The container environment itself: ```default A workshop is a development environment running in a container. ``` SDK - Always uppercase. Plural: SDKs (no apostrophe). interface - Lowercase when referring to the general concept; specific interfaces follow same pattern: - camera interface - GPU interface - mount interface **Command names** Always use exact command syntax: ```default workshop launch workshop connect workshopctl sdkcraft build ``` **Substitutions and reusable content** **Text substitutions** Use defined substitutions from `docs/reuse/substitutions.txt`: - `|ws_markup|` renders as Workshop (with `:program:` markup) - `|sdk_markup|` renders as SDKcraft (with `:program:` markup) **Reusable link references** Common external URLs are defined in `docs/reuse/links.txt`: - ``GitHub`_` links to the Workshop repository - ``LXD`_` links to LXD documentation - ``SDKcraft`_` links to SDKcraft repository - ``Releases`_` links to Workshop releases page These files are automatically included via the `docs/conf.py` configuration and are available in all reStructuredText documentation files. Using them ensures consistency and makes it easy to update URLs in a single location. **Punctuation** En dash (–): Use to represent a range or connection between two related items: ```default pages 10–15 East–West traffic Ubuntu 22.04–24.04 ``` Em dash (—): Avoid using em dashes. If possible, rephrase the sentence using other punctuation or sentence structure. **Command line terminology** Convention: Use [POSIX utility conventions](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html) when discussing command-line syntax, options, arguments, and other CLI elements. --- ## Project descriptions **Short description (79 characters)** ```text Workshop is a tool for defining and handling ephemeral development environments ``` **SDKcraft short description (77 characters)** ```text SDKcraft is a tool that packages and publishes SDKs to be used with Workshop ``` --- ## Documentation quality principles **Clarity** - State assumptions explicitly - Define prerequisites clearly - Avoid jargon without explanation - Use consistent terminology **Usability** - Focus on actionable information - Use direct imperatives for instructions - Break complex tasks into clear steps - Provide working examples **Precision** - Avoid ambiguous language - Use exact commands and syntax - Specify versions when relevant - Maintain consistent structure --- ## Contributing When contributing documentation: 1. Follow established patterns for file naming and structure 2. Use semantic line breaks in reStructuredText files 3. Include required metadata blocks 4. Add artefact markers for new concepts 5. Test examples before including them 6. Run documentation builds locally to verify For detailed contribution guidelines, see [How to contribute](https://ubuntu.com/workshop/docs//contributing.md#contributing) in the documentation. # explanation.md # Explanation These explanatory articles cover the main building blocks of **Workshop**. To start using **Workshop** and **SDKcraft**, it is important to understand how these concepts fit together. ## Architecture **Workshop**’s architecture combines a set of system components with well-defined runtime behavior to provide container-isolated development environments. * [Architecture](https://ubuntu.com/workshop/docs//explanation/architecture/index.md) * [System components](https://ubuntu.com/workshop/docs//explanation/architecture/components.md) * [Runtime behavior](https://ubuntu.com/workshop/docs//explanation/architecture/runtime-behavior.md) ## Workshops and projects Workshops are development environments, each running in a container, mapping your project to its contained dependencies. In turn, a project is a working directory where multiple workshop definitions can be placed. * [Workshops](https://ubuntu.com/workshop/docs//explanation/workshops/index.md) * [Workshop concepts](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md) * [Projects](https://ubuntu.com/workshop/docs//explanation/workshops/projects.md) * [Multi-workshop patterns](https://ubuntu.com/workshop/docs//explanation/workshops/multi-workshop-patterns.md) * [Changes, tasks](https://ubuntu.com/workshop/docs//explanation/workshops/changes-tasks.md) * [workshop (CLI)](https://ubuntu.com/workshop/docs//explanation/workshops/workshop-cli.md) ## SDKs SDKs are packages of software dependencies that can be installed in workshops to create tailored development environments. * [SDKs](https://ubuntu.com/workshop/docs//explanation/sdks/index.md) * [SDK concepts](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md) * [Parts](https://ubuntu.com/workshop/docs//explanation/sdks/parts.md) * [Design best practices](https://ubuntu.com/workshop/docs//explanation/sdks/best-practices.md) * [SDKs versus Dockerfiles](https://ubuntu.com/workshop/docs//explanation/sdks/sdk-vs-dockerfile.md) * [sdk (CLI)](https://ubuntu.com/workshop/docs//explanation/sdks/sdk-cli.md) * [sdkcraft (CLI)](https://ubuntu.com/workshop/docs//explanation/sdks/sdkcraft-cli.md) * [workshopctl (CLI)](https://ubuntu.com/workshop/docs//explanation/sdks/workshopctl-cli.md) ## Interfaces Interfaces allow communication and resource sharing between a workshop and the host system, as well as between the different SDKs that are part of a workshop. * [Interfaces](https://ubuntu.com/workshop/docs//explanation/interfaces/index.md) * [Interface concepts](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md) * [Camera interface](https://ubuntu.com/workshop/docs//explanation/interfaces/camera-interface.md) * [Custom device interface](https://ubuntu.com/workshop/docs//explanation/interfaces/custom-device-interface.md) * [Desktop interface](https://ubuntu.com/workshop/docs//explanation/interfaces/desktop-interface.md) * [GPU interface](https://ubuntu.com/workshop/docs//explanation/interfaces/gpu-interface.md) * [Mount interface](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md) * [SSH interface](https://ubuntu.com/workshop/docs//explanation/interfaces/ssh-interface.md) * [Tunnel interface](https://ubuntu.com/workshop/docs//explanation/interfaces/tunnel-interface.md) ## Security considerations This overview discusses the security aspects of **Workshop** and **SDKcraft**, such as isolation, privileges, relevant risks, and interface mechanics. See [Security policy](https://ubuntu.com/workshop/docs//security.md). # fix-installation.md # How to troubleshoot **Workshop** If you notice issues with workshops, projects, or **Workshop** in general, it may be time to verify or update the installation or prerequisites. ## Check and update version Ensure your version matches the latest [release](https://github.com/canonical/workshop/releases/) on GitHub: ```console $ snap info workshop ... installed: 0.1.13 (x18) 24MB classic ``` If it’s outdated, upgrade the installation: [Upgrade instructions](https://ubuntu.com/workshop/docs//release-notes/index.md#release-upgrade). ## Install and start LXD A major prerequisite for **Workshop** is [LXD](https://documentation.ubuntu.com/lxd/latest/); ensure it’s installed and running: ```console $ sudo snap install --channel=6/stable lxd $ sudo snap start --enable lxd.daemon $ sudo snap services lxd ``` You may need to add yourself to the `lxd` group to access its resources: ```console $ sudo usermod -a -G lxd $USER ``` As a final step, see the [troubleshooting guides](https://documentation.ubuntu.com/lxd/latest/howto/troubleshoot/) in LXD documentation. ## Check the snap logs Before resorting to the [debugging guide](https://ubuntu.com/workshop/docs//how-to/fix-workshops/debug-issues.md#how-debug-issues-workshops) for individual workshops, review the snap’s logs: ```console $ sudo snap logs workshop ``` ## Explore LXD containers If you notice an issue with a specific workshop, use the [LXC](https://documentation.ubuntu.com/lxd/latest/explanation/lxd_lxc/) utility to identify and troubleshoot it. For instance, if you’ve deleted a project without first removing the associated workshops, you can list all LXD projects to locate the orphaned containers. These will appear under `workshop.` and include the **Workshop** project ID in their names: ```console $ sudo lxc list --all-projects ... | workshop.user | nimble-ec275767 | STOPPED | | | CONTAINER | 0 | ``` Next, you can manually delete a container: ```console $ sudo lxc delete nimble-ec275767 --project workshop.user ``` Or, you can shell into the container to recover its data: ```console $ sudo lxc shell nimble-ec275767 --project workshop.user ``` If the container fails to start, use **lxc info** to view the latest log entries: ```console $ sudo lxc info --show-log nimble-ec275767 --project workshop.user ``` To increase log verbosity, you can reconfigure LXD with **snap**: ```console $ sudo snap set lxd daemon.debug=true $ sudo snap restart lxd.daemon ``` Use other relevant [LXC](https://documentation.ubuntu.com/lxd/latest/explanation/lxd_lxc/) commands to continue your investigation. ## See also How-to guides: - [How to debug issues in workshops](https://ubuntu.com/workshop/docs//how-to/fix-workshops/debug-issues.md#how-debug-issues-workshops) # fix-workshops.md # How to fix workshops Troubleshooting a misbehaving workshop usually starts with tracing the root cause, then either repairing it in place or rebuilding from scratch. ## Diagnose and resolve issues When a workshop misbehaves, start by tracing the root cause. If the problem involves resource conflicts between SDKs, the plug conflict guide walks through that specific scenario: * [Debug issues in workshops](https://ubuntu.com/workshop/docs//how-to/fix-workshops/debug-issues.md) * [Resolve plug conflicts](https://ubuntu.com/workshop/docs//how-to/fix-workshops/resolve-plug-conflicts.md) ## Repair or reset If debugging doesn’t help, the issue may lie in the **Workshop** installation itself, or the workshop may need to be purged and recreated from scratch: * [Fix the installation](https://ubuntu.com/workshop/docs//how-to/fix-workshops/fix-installation.md) * [Purge workshops](https://ubuntu.com/workshop/docs//how-to/fix-workshops/purge.md) # forward-ports.md # How to forward ports with tunneling Port-forwarding in **Workshop** is done with the *tunnel interface*. Tunnels pair a *plug* (the listening side) with a *slot* (the service side), forwarding every connection that reaches the plug address to the slot address. Three common scenarios cover most day-to-day port-forwarding needs. ## Expose workshop services To expose a service running inside a workshop, add a tunnel slot to the SDK that runs the service and a matching plug to the `system` SDK: ```yaml sdks: - name: go slots: caddy: interface: tunnel endpoint: localhost:8080 # service in the workshop - name: system plugs: caddy: interface: tunnel endpoint: localhost:8080 # port on the host ``` Refresh the workshop and start the service, so the host can reach it at `localhost:8080`: ```console $ workshop refresh ``` Note that port numbers can be different from each other, subject to the regular low-port limitations. Ensure the plug port is free before refreshing, or the tunnel will fail to activate. #### NOTE **Workshop** doesn’t resolve hostnames, but supports the aliases `localhost`, `ip6-localhost`, and `ip6-loopback`. ## Share host services When a service runs on the host and code inside the workshop needs it, create the tunnel the other way around: a slot in the `system` SDK (the provider) and a plug in the regular SDK (the consumer). The example shares the host’s PostgreSQL server (`localhost:5432`) with MLflow in the workshop: ```yaml sdks: - name: mlflow plugs: postgres: interface: tunnel endpoint: localhost:5432 # where MLflow will connect - name: system slots: postgres: interface: tunnel endpoint: localhost:5432 # host PostgreSQL server ``` Refresh the workshop to pick up the changes: ```console $ workshop refresh ``` One notable difference is that the connection validation policies are more strict when the slot is defined on the host, so an extra command is needed to connect the plug to the slot: ```console $ workshop connect mlflow/mlflow:postgres mlflow/system:postgres ``` After this, **Workshop** validates the endpoints and enables the connection. MLflow can now reach PostgreSQL at `localhost:5432`. The same pattern works for any host-side TCP- or UDP-based service. ## Cross-protocol forwarding Tunnels are not limited to identical protocols on both ends. Unix domain sockets are often used for local-only daemons. The tunnel interface lets you bridge them to TCP ports and vice versa. Why do this? - Avoid port clashes: Listen on a unique Unix path and publish it on an arbitrary TCP port. - Expose a local service: Make a Unix-only daemon visible to tools that only speak TCP. #### NOTE Only TCP and Unix domain sockets can be bridged across a tunnel. UDP is not compatible with Unix domain sockets. ### Workshop Unix domain socket to host TCP port Suppose a gRPC service inside the workshop listens on `/run/grpc-service.sock` (Unix). You want to reach it on the host at `localhost:18080`: ```yaml #... sdks: - name: grpc-service slots: api: interface: tunnel endpoint: /run/grpc-service.sock # Unix domain socket in the workshop - name: system plugs: api: interface: tunnel endpoint: localhost:18080 # chosen TCP port on the host ``` After a refresh, the service will be reachable from the host at `grpc://localhost:18080`: ```console $ workshop refresh $ workshop info ... sdks: system: tunnels: api: from: 127.0.0.1:18080/tcp to: /run/grpc-service.sock ... ``` #### NOTE The tunnel interface expands `$HOME` and `$XDG_RUNTIME_DIR` in socket file paths automatically, but refuses other variables. Only user-writable locations are accepted for security reasons. ### Host Unix domain socket to workshop TCP port Now let’s invert the flow. Share a host abstract socket (which exists only in the kernel, not on disk) with code inside the workshop on TCP port `9000`. ```yaml #... sdks: - name: system slots: bus: interface: tunnel endpoint: '@bus' # abstract socket on the host - name: client plugs: bus: interface: tunnel endpoint: localhost:9000 # TCP port inside workshop ``` After **workshop refresh** and **workshop connect**, the code in the workshop can connect to `localhost:9000`, and **Workshop** forwards the traffic to the host’s abstract socket `@bus`. #### NOTE Abstract sockets avoid filesystem permissions and name collisions. They are written as `@name` (note the leading “@”). ## Troubleshooting - TCP to Unix bridging is supported, while UDP to Unix is not. - Ports below 1024 (privileged ports) may be rejected on the host side. - Ensure the slot socket addresses exist and can be accessed by the **Workshop** user; plug sockets are created by **Workshop** so they shouldn’t be already occupied. - The tunnel won’t activate if either side’s endpoint is invalid; see error messages and **workshop tasks** for hints. ## See also Explanation: - [System SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-system-sdk) - [Connection](https://ubuntu.com/workshop/docs//explanation/interfaces/tunnel-interface.md#exp-tunnel-connection) - [Tunnel interface](https://ubuntu.com/workshop/docs//explanation/interfaces/tunnel-interface.md#exp-tunnel-interface) Reference: - [Tunnel interface](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-tunnel-interface) - [workshop connect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connect) - [workshop disconnect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-disconnect) - [workshop tasks](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-tasks) # gpu-interface.md # GPU interface The GPU interface enables GPU pass-through (direct access to the host system’s GPUs) inside the workshop to improve the performance of GPU-intensive applications. By using the interface, the SDK publisher allows the workshop to directly access the host’s GPU devices, which may be required for various GPU-intensive workloads. ## GPU interface plug An essential element here is the GPU interface plug, which is declared in the SDK definition. Its structure includes just the name of the plug and the interface; both must be set to `gpu`. Defining the plug in an SDK allows the workshops using this SDK to directly access the host’s GPU devices, which may be required for various GPU-intensive workloads. ## GPU interface slot To let SDKs in a workshop access the host’s GPUs, **Workshop** provides a GPU interface slot that multiple GPU interface plugs can access. When the SDK is installed at runtime during launch and refresh operations, **Workshop** checks that the plug targeting the slot passes [validation](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interfaces-validation); if it does, it can be connected. ## Connection The interface is connected automatically at launch or refresh, provided that the plug can be matched to the slot by its name or via a `connections` entry in the [definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition), both subject to **Workshop**’s [validation rules](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interfaces-validation). After the workshop has started, the **workshop connect** and **workshop disconnect** commands can be used to manage the connection manually. Establishing a connection means the host’s GPUs are directly available inside the workshop via the GPU pass-through mechanism. To check if the interface is connected: ```console $ workshop connections --all INTERFACE PLUG SLOT NOTES ... gpu ws/gpu-sdk:gpu ws/system:gpu - ``` This means the host’s GPUs are directly available inside the workshop: ```console $ workshop shell ws workshop@ws-8584e571$ ls -h /dev/dri/ card0 renderD128 workshop@ws-8584e571$ nvidia-smi ``` ## See also Explanation: - [Interface concepts](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-concepts) - [Plugs and slots](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-plugs-slots) - [SDK definition](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-definition) - [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) Reference: - [workshop connect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connect) - [workshop connections](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connections) - [workshop disconnect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-disconnect) - [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) - [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh) - [workshop shell](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-shell) # gpu.md # GPU interface The GPU interface exposes host GPU devices. - Plug attributes: none. - Plug name: must be `gpu`. - Plug owner: any regular SDK; not the system SDK. - Slot: the system SDK provides a single `system:gpu` slot. Other SDKs cannot declare GPU slots. # how-to.md # How-to guides These articles cover the needs and corner cases that arise when you use **Workshop** and **SDKcraft**. ## Customize workshops Daily **Workshop** usage may involve multiple one-off scenarios: moving projects within the filesystem, adding custom actions, or running multiple workshops side by side: * [Customize workshops](https://ubuntu.com/workshop/docs//how-to/customize-workshops/index.md) * [Add actions to workshops](https://ubuntu.com/workshop/docs//how-to/customize-workshops/add-actions.md) * [Add mounts](https://ubuntu.com/workshop/docs//how-to/customize-workshops/add-mounts.md) * [Forward ports](https://ubuntu.com/workshop/docs//how-to/customize-workshops/forward-ports.md) * [Move projects around](https://ubuntu.com/workshop/docs//how-to/customize-workshops/move-projects.md) * [Use multiple workshops](https://ubuntu.com/workshop/docs//how-to/customize-workshops/use-multiple-workshops.md) ## Develop with workshops **Workshop** integrates with developer tooling; AI agents, IDEs, version control, and CI/CD workflows all work inside a workshop: * [Develop with workshops](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/index.md) * [Connect VS Code to a workshop](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/connect-vscode.md) * [Run JetBrains Gateway in a workshop](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/run-jetbrains-gateway.md) * [Run JupyterLab in your browser](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/run-jupyterlab-in-browser.md) * [Manage Python environments](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/manage-python-environments.md) * [Run GitHub Actions locally](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/run-github-actions-locally.md) * [Run workshops in GitHub Actions](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/run-workshops-in-github-actions.md) * [Use workshops with AI agents](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/use-workshops-with-ai-agents.md) * [Use workshops with Git](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/use-git.md) ## Develop SDKs These guides cover the work of authoring an SDK with **SDKcraft** and publishing it to the SDK Store: * [Develop SDKs](https://ubuntu.com/workshop/docs//how-to/develop-sdks/index.md) * [Build an SDK](https://ubuntu.com/workshop/docs//how-to/develop-sdks/build-an-sdk.md) * [Publish an SDK](https://ubuntu.com/workshop/docs//how-to/develop-sdks/publish-an-sdk.md) ## Fix workshops Troubleshooting covers issues with running workshops and with the **Workshop** installation itself: * [Fix workshops](https://ubuntu.com/workshop/docs//how-to/fix-workshops/index.md) * [Debug issues in workshops](https://ubuntu.com/workshop/docs//how-to/fix-workshops/debug-issues.md) * [Resolve plug conflicts](https://ubuntu.com/workshop/docs//how-to/fix-workshops/resolve-plug-conflicts.md) * [Fix the installation](https://ubuntu.com/workshop/docs//how-to/fix-workshops/fix-installation.md) * [Purge workshops](https://ubuntu.com/workshop/docs//how-to/fix-workshops/purge.md) # interfaces.md # Interfaces Interfaces allow communication and resource sharing between a workshop and the host system, as well as between the different SDKs that are part of a workshop. ## General concepts Start here to understand how interfaces use plugs and slots to connect SDKs to host resources and to each other: * [Interface concepts](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md) ## Hardware interfaces Hardware interfaces give workshops access to host hardware such as displays, GPUs, and cameras: * [Camera interface](https://ubuntu.com/workshop/docs//explanation/interfaces/camera-interface.md) * [Custom device interface](https://ubuntu.com/workshop/docs//explanation/interfaces/custom-device-interface.md) * [Desktop interface](https://ubuntu.com/workshop/docs//explanation/interfaces/desktop-interface.md) * [GPU interface](https://ubuntu.com/workshop/docs//explanation/interfaces/gpu-interface.md) ## Data and connectivity Filesystem mounts, SSH agent forwarding, and network sharing pass through this group of interfaces: * [Mount interface](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md) * [SSH interface](https://ubuntu.com/workshop/docs//explanation/interfaces/ssh-interface.md) * [Tunnel interface](https://ubuntu.com/workshop/docs//explanation/interfaces/tunnel-interface.md) # manage-python-environments.md # How to manage Python environments with the uv SDK The `uv` SDK is the recommended way to manage Python projects in **Workshop**. It ships **uv** and **uvx**, aliases **pip** to **uv pip** for compatibility, and exposes a virtual environment slot that other Python-based SDKs (such as `jupyter`) can plug into. The typical day-to-day flow boils down to running **uv** commands against a project virtual environment whose location you control. ## Add the uv SDK to your workshop Add `- name: uv` to the workshop definition; the SDK tracks the `latest/stable` channel by default, which is sufficient for most projects: ```yaml name: pyenv base: ubuntu@24.04 sdks: - name: uv ``` Place your project at the host directory that you launch the workshop from; **Workshop** bind-mounts it inside the workshop at `/project/`. The project should contain a `pyproject.toml` (or a `requirements.txt`) that **uv** can read. ## Run uv from your project Open a shell in the workshop and use **uv** from `/project/`: ```console $ workshop shell workshop@pyenv:~$ cd /project workshop@pyenv:/project$ uv sync workshop@pyenv:/project$ uv add requests workshop@pyenv:/project$ uv run python -c "import requests" ``` **uv sync** resolves the project’s dependencies and creates a virtual environment at `/project/.venv`, that is, next to your `pyproject.toml`. **uv run** and **uv add** then operate against that environment. The venv lives on the host filesystem because `/project/` is the bind mount, so it survives container refreshes and is visible to host-side tooling such as IDEs. The `uv` SDK aliases **pip** to **uv pip** through **update-alternatives**; running **pip install ** from a workshop shell transparently invokes **uv pip install **. ## Inspect what the SDK configures The SDK applies a few defaults so that **uv** works correctly with the workshop’s storage layout: - `UV_LINK_MODE` is set to `copy` in the workshop user’s environment because the persistent cache mount does not support hardlinks. You don’t need to set this yourself. - The **uv** package cache is persisted on the host through a `mount` interface plug that maps `/home/workshop/.cache/uv/` to durable storage, so cached downloads survive workshop updates. - A shared virtual environment slot is exposed at `/home/workshop/uv-venv/`. This slot exists for cross-SDK sharing (see the next section); it is *not* the venv your project uses by default. ## Share the environment with another SDK To run a Python SDK such as JupyterLab against a uv-managed environment, add the `jupyter` SDK alongside `uv` and connect `jupyter:venv` to `uv:venv` explicitly in the workshop definition: ```yaml name: pyenv base: ubuntu@24.04 sdks: - name: uv - name: jupyter connections: - plug: jupyter:venv slot: uv:venv ``` An explicit `connections:` block is required: without it, `jupyter:venv` falls back to `system:mount` (the host directory **Workshop** provides as a default plug target) and the two SDKs don’t share an environment. After **workshop refresh**, **workshop connections --all** confirms the wiring: ```console $ workshop connections --all INTERFACE PLUG SLOT NOTES mount pyenv/jupyter:venv pyenv/uv:venv - mount pyenv/uv:cache pyenv/system:mount - ``` Packages that `jupyter` installs into its venv now land in the shared environment provided by `uv`, so **jupyter** and **uv run** see the same dependency set. ## Pin the project venv with UV_PROJECT_ENVIRONMENT By default, **uv** places the project virtual environment at `.venv` next to the `pyproject.toml` it discovers, which inside a workshop is normally `/project/.venv`. Override this default with `UV_PROJECT_ENVIRONMENT` when you want a single, explicit venv location regardless of where in the project tree **uv** is invoked, or when several workshops share the same project directory and should reuse one venv. For example, to place the venv at `/project/pinned-venv` instead of the default `/project/.venv`: ```console $ echo 'export UV_PROJECT_ENVIRONMENT=/project/pinned-venv' >> ~/.profile $ exec bash -l $ cd /project $ uv sync ``` Relative paths are resolved from the workspace root, absolute paths are used as is; if the environment does not exist at the specified path, **uv** creates it. #### WARNING Set `UV_PROJECT_ENVIRONMENT` to a path *inside* `/project/`, such as `/project/.venv/`, not to `/project/` itself. **uv** writes the venv layout (`bin/`, `lib/`, `pyvenv.cfg`) directly under the value you provide, so a bare `/project/` would scatter venv files across your project sources. ## See also Explanation: - [SDK dependencies](https://ubuntu.com/workshop/docs//explanation/sdks/best-practices.md#exp-best-dependencies) - [Mount interface](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md#exp-mount-interface) - [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) How-to guides: - [How to run JupyterLab in your browser](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/run-jupyterlab-in-browser.md#how-jupyterlab-run-in-browser) # mount-interface.md # Mount interface The mount interface securely exposes filesystem locations on the host (via the [system SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-system-sdk) only) or in the workshop by mounting them inside the workshop. By using the interface, the SDK publisher enables the use of the mount mechanism in the workshop; a host location also allows persisting data outside the workshop. The interface defines a target directory inside the workshop, to which a source directory is mounted at runtime. Typically, it would provide resources to be consumed by the SDK, accumulated over time or created when the **workshop launch** or **workshop refresh** commands run: - The slot is the provider, indicating that any data placed in its source directory can be used by a workshop via a plug. - The plug is the consumer, indicating that the data will be available at the target directory, where the SDK, or the user presumably expects it. ## Mount interface plug An essential element here is the mount interface plug, which is declared in the SDK definition. A basic structure would include the name of the plug itself, the interface (`mount`) the intended target path inside the workshop (`workshop-target`) and, optionally, whether the mount should be read-only (`read-only`). **Workshop** will create the target path if it doesn’t exist. Plugs may customize the permissions (`mode`) and ownership (`uid`, `gid`) of any directories created. Defining the plug in an SDK designates the target directory inside the workshop; a directory on the host system that **Workshop** will create at runtime will be mounted to it. This allows the workshops using this SDK to use the host directory (which **Workshop** allocates automatically and doesn’t expose otherwise) to persist the files placed there from inside the workshop in the host filesystem when the workshop stops. ## Mount interface slot To let SDKs in a workshop access the host filesystem, **Workshop** provides a mount interface slot that multiple mount interface plugs can access. When the SDK is installed at runtime during launch and refresh operations, **Workshop** checks the following for each plug that targets the slot: - The plug can be installed. - The plug can be auto-connected (for `mount`, it’s a yes). - The `workshop-target` directory already exists in the workshop. If the plug passes the checks, it is connected. ## Connection The interface is connected automatically at launch or refresh if the plug can be matched to the slot by its name or via a `connections` entry in the [definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition), both subject to **Workshop**’s [validation rules](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interfaces-validation). Establishing a connection means the source directory created by **Workshop** is mounted to the target directory inside the workshop. The source directory can be created: - At a designated path inside the workshop, which needs a slot with `workshop-source` set - At an internal location on the host, which **Workshop** assigns if no slot is set explicitly If the directory is created on the host, its contents are preserved between operations such as **workshop refresh**, **workshop start**, and **workshop stop**. After the workshop has started, the **workshop connect** and **workshop disconnect** commands can be used to manage the connection manually. To check if the interface is connected: ```console $ workshop connections --all INTERFACE PLUG SLOT NOTES ... mount ws/mount-sdk:cache :cache manual ``` This means a source directory is mounted to the target: ```console $ workshop info ws name: ws base: ubuntu@22.04 project: /home/user/workshops/ws status: ready notes: - sdks: mount-sdk: tracking: latest/edge installed: 2022-03-04 (1) mounts: cache: host-source: .../8584e571/ws/mount/mount-sdk/cache workshop-target: /home/workshop/.local/cache ``` Here, the source is set to an internal location (`...`) that **Workshop** maintains on the host filesystem; the SDKs can’t set host locations explicitly for security reasons, but there’s a way to do it manually. ## Remount The **workshop remount** command sets a new source directory on the host for the target directory inside the workshop: ```console $ workshop remount ws/mount-sdk:cache ~/.local/cache/ ``` First, the remount operation is attempted atomically; this usually succeeds if the new source is either a nonexistent directory or an empty directory on the same filesystem as the current source. Otherwise, the remount only occurs if the workshop has been stopped earlier, which prevents data corruption. To reset a remounted plug to its default source location, use `workshop disconnect` with the `--forget` option, then refresh the workshop: ```console $ workshop disconnect ws/mount-sdk:cache --forget $ workshop refresh ws ``` ## See also Explanation: - [Interface concepts](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-concepts) - [Plugs and slots](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-plugs-slots) - [SDK definition](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-definition) - [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) Reference: - [workshop connect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connect) - [workshop connections](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connections) - [workshop disconnect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-disconnect) - [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) - [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh) - [workshop remount](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-remount) - [workshop start](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-start) - [workshop stop](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-stop) # mount.md # Mount interface The mount interface exposes a directory between a slot owner and a plug owner. A mount plug is described by these attributes: | Key | Value | Description | |------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `workshop-target` (required) | string | Path inside the workshop used as the plug’s target directory.
Must be an absolute path;
`$SDK` expands to the SDK’s installation path in the workshop. | | `mode` | integer | File permissions, in octal, applied when creating `workshop-target`
and any missing parent directories.
Defaults to `0o775` for regular users.
When `uid` is zero, defaults to `0o755`. | | `uid` | integer | User ID applied when creating `workshop-target`
and any missing parent directories.
Defaults to `1000` when `workshop-target` is under
`/home/workshop/`, `/project/`, or `/run/user/1000/`.
Defaults to `0` otherwise. | | `gid` | integer | Group ID applied when creating `workshop-target`
and any missing parent directories.
Defaults to `1000` or `0`
by the same path rule as `uid`,
even when `uid` is set explicitly. | | `read-only` | Boolean | Whether the target directory should be read-only.
Defaults to `false`. | Plug owner: any regular SDK; not the system SDK. The system SDK provides one mount slot, `system:mount`, with a dynamic `host-source` attribute that can be configured only at [remount](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-remount). It is the only mount slot whose source is on the host filesystem. A mount slot on a regular SDK is described by this attribute: | Key | Value | Description | |------------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| | `workshop-source` (required) | string | Path inside the workshop used as the slot’s source directory.
Must be an absolute path;
`$SDK` expands to the SDK’s installation path in the workshop. | # move-projects.md # How to move projects around It may be unclear how workshops react to everyday operations such as moving or copying a project directory. Let’s spend some time talking about different aspects of this. ## Before launch A workshop that you didn’t launch is just a definition file that behaves like any good file should. Things change *after* you run **workshop launch**: ```yaml name: golang base: ubuntu@22.04 sdks: - name: go channel: 1.26 ``` ```console $ workshop launch --project /home/user/old/ ``` ## Move a project This is the simplest scenario. Start in the same project directory where you launched the workshop: ```console $ workshop list --global PROJECT WORKSHOP STATUS NOTES /home/user/old golang Ready - ``` Move the project directory and check the workshop: ```console $ mv /home/user/old/ /home/user/new/ $ workshop list --global PROJECT WORKSHOP STATUS NOTES /home/user/new golang Ready - ``` **Workshop** handles the project’s move gracefully so the workshop here remains as you would expect; there are no loose ends to pick up, no paths to update in your definition file. However, this only ensures the safe transition of the workshop itself, so it’s up to you to update any paths external to **Workshop** that point to the project’s previous location. ## Copy a project Now let’s copy the project directory. Again, start with the workshop’s location: ```console $ workshop list --global PROJECT WORKSHOP STATUS NOTES /home/user/old golang Ready - ``` Copy the project directory and check the workshops: ```console $ cp -r /home/user/old/ /home/user/new/ $ workshop list --global PROJECT WORKSHOP STATUS NOTES /home/user/old golang Ready - ``` **Workshop** won’t launch the workshop in the new directory, which is probably the sensible default here, but what happens if you do it yourself? ```console $ workshop launch --project /home/user/new/ $ workshop list --global PROJECT WORKSHOP STATUS NOTES /home/user/old golang Ready - /home/user/new golang Ready - ``` Now, these are two independent workshops that happen to have the same name, not a single workshop that is somehow shared by multiple project directories. Again, it’s up to you to update any paths external to **Workshop** that should point to your new project. ## Remove a project **Workshop** doesn’t handle file deletion automatically; make sure you remove all workshops before deleting the project directory: ```console $ workshop remove --project /home/user/old/ $ rm -rf /home/user/old/ ``` ## See also Explanation: - [Projects](https://ubuntu.com/workshop/docs//explanation/workshops/projects.md#exp-projects) - [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) Reference: - [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) - [workshop list](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-list) - [workshop remove](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-remove) # multi-workshop-patterns.md # Multi-workshop patterns One workshop per project is the default, but real work often pulls in more than one. A monorepo holds a Go backend and a Node frontend, each with its own toolchain. A coding agent needs to run over a branch without seeing sibling branches or unrelated host directories. Two long builds must progress without blocking your editing. A regression has to be confirmed against a new base image without disturbing the working setup. **Workshop** supports these cases through two patterns that combine the same primitives in different ways: several workshops sharing one project directory, or several project directories each hosting one or more workshops of their own. ## Two patterns The two patterns differ in where the project boundary falls: within a single project directory, or across several directories. Both rely on the same underlying mechanics; they’re distinguished by intent and topology, not by special CLI flags. ### One project, multiple workshops Here, several workshops live in a single project directory and share the project files mounted at `/project/`. Their definitions sit in the `.workshop/` subdirectory, each in its own file named after the workshop: ```none my-project/ ├── .workshop/ │ ├── frontend.yaml │ ├── backend.yaml │ └── common-tools/ │ └── sdk.yaml ├── web/ └── api/ ``` Each workshop has its own base image, its own SDK list, and its own actions, but they all see the same project files. [In-project SDKs](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-in-project-sdk), stored as subdirectories of `.workshop/`, can be referenced by *any* workshop in the project and provide a clean way to share custom tooling. The workshops are isolated as LXD containers: each has its own filesystem layers, its own running processes, and its own snapshot chain. What’s shared is the project content on the host, not the runtime. Direct interface connections between two workshops are rejected by **workshop connect**; if your workshops need to talk to each other, they may do so by bridging through the host with two independent [tunnel](https://ubuntu.com/workshop/docs//explanation/interfaces/tunnel-interface.md#exp-tunnel-interface) connections. Pick this pattern when several components of one project need different runtimes but the same source tree. ### Multiple projects, one workshop each Here, you work on multiple related project directories. Each project directory has its own `workshop.yaml` (or its own `.workshop/` subdirectory) and its own `.workshop.lock`. Workshops in different projects are fully independent: the project files they mount at `/project/` differ, the containers are separate, and the workshop definitions can diverge freely. In day-to-day work this pattern is most often realized with [`git worktree`](https://git-scm.com/docs/git-worktree), which gives you several working trees of the same repository in sibling directories. Each worktree is a distinct project from **Workshop**’s point of view, gets its own project ID, and can run a workshop that’s named identically to its sibling without any collision. The **workshop list** command, invoked with `--global`, shows the workshops across the projects currently tracked by **Workshop**, including their project paths. Pick this pattern when the parts have to stay separated: different branches, different snapshots of the codebase, different base images for the same code, or different agents whose blast radius should not overlap. ### At a glance | | One directory, multiple workshops | Multiple directories, one workshop each | |---------------------------|-----------------------------------------------------|------------------------------------------------------------------------------------| | Definition location | `.workshop/.yaml` per workshop | `workshop.yaml`, or `.workshop/`, per directory | | Mounted at `/project/` | The same directory in every workshop | A different directory in each workshop | | Project files | Shared across workshops | Isolated per directory | | Containers and snapshots | One per workshop | One per workshop | | Cross-workshop networking | Bridge through the host with two tunnels | Same; each workshop reaches the others through the host
like any other service | | Branch isolation | All workshops see whatever branch the project is on | Each directory pins its workshops to its branch | | Typical trigger | Polyglot components of one codebase | Parallel work on different branches or snapshots | ## Scenarios The patterns above answer most multiworkshop cases. The list below names the recurring ones, points to the pattern that fits, and links to the how-to that covers the mechanics. - *Polyglot project components.* A monorepo with parts in different languages or runtimes, for example a Go backend with a Node frontend, or Python data preparation alongside CUDA training and a Rust serving layer. Each component gets its own workshop in the same project; the project files are shared, the toolchains are not. - *Shared internal tooling across components.* When several workshops in one project rely on the same linter, formatter, or generator, package it as an [in-project SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-in-project-sdk) rather than duplicating the setup in each workshop definition. - *Parallel work on independent branches.* Two unrelated features, a feature and a hotfix, or a feature and a pull-request review. Each branch is checked out in its own worktree, and each worktree runs its own workshop. Editing, building, and any agent activity in one worktree has no effect on the others. - *A/B comparison of bases or SDK channels.* Two worktrees hold the same code but their workshop definitions differ in `base` or in SDK `channel`, letting you confirm a dependency bump, reproduce a version-specific bug, or evaluate a base image upgrade side by side with the working setup. - *Confinement for code-running agents.* A coding agent in a worktree or a subdirectory sees only that slice of the repository; sibling worktrees and unrelated host directories stay out of reach. The workshop container adds a second isolation boundary beneath the worktree boundary, so even an agent invoked with relaxed permission flags cannot reach the host filesystem outside of what the workshop mounts. - *Long-running task offload.* A build, training run, migration, or large refactor keeps progressing in one workshop while you continue editing in another. Use one project with multiple workshops if the task should see your current edits, or separate projects via worktrees if it should be pinned to a specific commit. - *Local reproduction of CI failures.* Check out the failing commit in a dedicated worktree and define a workshop with the base image CI uses. Your everyday workshop stays on the working setup and continues to run. ## Pattern combinations The two patterns combine. A monorepo with `.workshop/frontend.yaml` and `.workshop/backend.yaml` can also be checked out in two worktrees, each running both workshops. This is useful when reviewing a pull request on the monorepo’s structure while continuing development on the main branch: the review worktree has its own frontend and backend workshops, isolated from yours, and **workshop list** invoked with `--global` shows all four at once. ## See also Explanation: - [In-project SDKs](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-in-project-sdk) - [Projects](https://ubuntu.com/workshop/docs//explanation/workshops/projects.md#exp-projects) - [Tunnel interface](https://ubuntu.com/workshop/docs//explanation/interfaces/tunnel-interface.md#exp-tunnel-interface) - [Workshop concepts](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-concepts) How-to guides: - [How to forward ports with tunneling](https://ubuntu.com/workshop/docs//how-to/customize-workshops/forward-ports.md#how-forward-ports) - [How to use workshops with AI agents](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/use-workshops-with-ai-agents.md#how-use-workshops-with-ai-agents) Reference: - [Workshop definition](https://ubuntu.com/workshop/docs//reference/definition-files/workshop-definition.md#ref-workshop-definition) - [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) - [workshop list](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-list) # part-1-get-started.md # Get started with workshops This is the first section of the [four-part series](https://ubuntu.com/workshop/docs//tutorial/index.md#tut-index); a practical introduction that takes you on a tour of the essential **Workshop** activities. A *workshop* is a development environment running in a container, mapping your project to its contained dependencies. Here, you will practice all the major steps in the lifecycle of a workshop, from [defining](#tut-define), [launching](#tut-launch), and [refreshing](#tut-refresh) it to [executing commands](#tut-exec) and [shelling](#tut-shell) into the workshop. The steps you’re about to perform cover most of your daily needs with **Workshop**. ## Install **Workshop** Install **Workshop**, upgrading the prerequisites if needed, then ensure it runs. ### Prerequisites **Workshop** is supported on Ubuntu and other **snap**-enabled Linux distributions; it is also compatible with Windows Subsystem for Linux (WSL2), where it uses Btrfs instead of ZFS for storage. **Workshop** relies on [LXD 6.8+](https://canonical.com/lxd) for low-level operation and uses its [REST API](https://documentation.ubuntu.com/lxd/latest/restapi_landing/) to handle individual *workshops*. To install it from scratch with **snap**: ```console $ sudo snap install --channel=6/stable lxd ``` To refresh an existing **snap** installation: ```console $ sudo snap refresh --channel=6/stable lxd ``` #### NOTE If you prefer another installation method, see the available installation options in [LXD documentation](https://documentation.ubuntu.com/lxd/latest/installing/); after installation, make sure the [LXD daemon](https://documentation.ubuntu.com/lxd/latest/explanation/lxd_lxc/#lxd-daemon) is enabled and running. If in doubt, refer to LXD documentation and your distribution’s manuals for guidance. ### Installation Install the snap using the [--classic](https://snapcraft.io/docs/install-modes/) option: ```console $ sudo snap install --classic workshop ``` ## Launch a workshop Now you’ll learn how to define, launch, start and stop a workshop. ### Define, add SDKs First, you need to define a workshop. A definition is a YAML file that is stored in your project directory; it lists the components of the workshop to be instantiated at launch. A definition can list many moving parts; perhaps, the most important are SDKs, which are basic, predefined building blocks of your development environment. You reference SDKs from your workshop definition to specify what you want to include in your workshop. At runtime, **Workshop** pulls and installs them, providing the dependencies and packages required for your work, while keeping the SDKs themselves isolated and manageable. For demonstration purposes, assume we want to work with AI models using the [Ollama](https://ollama.com/) platform. To do this, let’s use the `ollama` SDK, which provides a local AI model server. Before adding an SDK to a workshop, search the SDK Store to confirm it exists and check its publisher and current version: ```console $ sdk find ollama NAME VERSION PUBLISHER SUMMARY ollama 0.20.2 Canonical Get up and running with large language models ``` The query also matches an SDK’s title, summary, description, or publisher, so a broader keyword like **sdk find ai** can surface AI-related SDKs on the Store. To see which channels and bases are available for a specific SDK, inspect its details: ```console $ sdk info ollama name: ollama publisher: Canonical (canonical) license: MIT Get up and running with Llama 3.3, ... CHANNELS CHANNEL VERSION BUILD BASE REV SIZE latest/stable 0.20.2 2026-04-15 ubuntu@24.04 7 2.27GB ubuntu@22.04 8 2.27GB ... cpu/stable 0.20.2 2026-04-15 ubuntu@24.04 2 15.22MB ubuntu@22.04 5 15.22MB cpu/candidate ^ cpu/beta ^ cpu/edge ^ ``` The `CHANNELS` table lists each track (here, `latest`, `cpu`, `cuda`, `rocm`, and `vulkan`) at four risk levels (`stable`, `candidate`, `beta`, `edge`), the bases each revision supports, and its on-disk size. For this tutorial, we’ll use `cpu/stable`, which runs on any machine without GPU hardware. For the project directory, create a new Python repository: ```console $ mkdir ollama-python-project $ cd ollama-python-project $ git init ``` Everything you handle with your workshop goes here: your Python code, custom assets, and so on. In the project directory, scaffold a workshop definition with **workshop init**, passing the base, the SDKs, and their channels on the command line: ```console $ workshop init dev --sdks ollama/cpu/stable --base ubuntu@22.04 "dev" workshop created at /home/user/ollama-python-project/.workshop/dev.yaml ``` Each `--sdks` entry can take the `/` form, so `ollama/cpu/stable` pins the `ollama` SDK to its `cpu/stable` channel. The command writes the definition to `.workshop/dev.yaml`: ```yaml name: dev base: ubuntu@22.04 sdks: - name: ollama channel: cpu/stable ``` Here, the specific version to retrieve from the SDK Store comes from the `cpu/stable` channel of the `cpu` track. To confirm that **Workshop** sees the definition, list the workshops in the project directory: ```console $ workshop list WORKSHOP STATUS NOTES dev Off - ``` #### NOTE **Workshop** ships shell completion for Bash, Zsh, Fish, and PowerShell; if you installed via snap, it is already enabled. Press Tab as you type any **workshop** command to autocomplete subcommands, flags, and arguments. Completion is context-aware: each command offers only values that make sense for it. For instance, **workshop start** autocompletes from *Stopped* workshops only, **workshop stop** from *Ready* ones, and **workshop connect** autocompletes available plugs and then the matching slots. As the command output suggests, your newly defined workshop is *Off*, so it needs to be launched. #### NOTE The command lists all workshops within the project; the tutorial focuses on a single-workshop setup, but your project can have multiple workshops defined. For a detailed explanation of the workshop status values, see the [Workshop status](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-status) section. #### NOTE The tutorial uses Ollama for demonstration purposes only. This doesn’t imply that **Workshop** is intended solely for AI; quite the contrary, it’s envisioned as language-neutral and framework-agnostic. ### Launch, start, and stop To get a workshop ready for use, you launch it: ```console $ workshop launch "dev" launched ``` Once the workshop is launched, you can start using it to build, debug, and run your code. After launching, check the runtime information to see what went into your workshop: ```console $ workshop info name: dev base: ubuntu@22.04 project: /home/user/ollama-python-project status: ready notes: - sdks: system: installed: (1) ollama: tracking: cpu/stable installed: 0.20.2 2026-04-15 (5) mounts: models: host-source: .../6b79e889/dev/mount/ollama/models workshop-target: /home/workshop/.ollama/models ``` The output looks like the [definition](#tut-define) with extra details such as the [mounts](https://ubuntu.com/workshop/docs//tutorial/part-2-work-with-interfaces.md#tut-interfaces); ignore these for now. While **workshop info** shows the SDKs from *one* workshop’s perspective, **sdk list** reports every SDK volume currently stored on the machine, regardless of which workshop pulled it: ```console $ sdk list NAME VERSION REV SIZE ollama 0.20.2 5 15.22MB system - 1 25.09kB ``` Each row is a distinct SDK volume on disk. Until you launch a workshop that references an SDK, it won’t appear here; at launch, **Workshop** has pulled the SDK from the SDK Store and the revision shown matches the one in **workshop info**. After launch, **Workshop** tracks the project directory using a hidden `.workshop.lock` file that must remain in the project directory and **not be copied or stored externally**, e.g., in a repository. You only need to launch a workshop once after defining it; after any substantial changes to it, you do a [refresh](#tut-refresh). Otherwise, the workshop is just a fancy container that can be started and stopped. The workshop starts automatically at launch, but you can also stop and restart it at will. Suppose you want to free up some resources, so you stop the workshop: ```console $ workshop stop ``` This changes the status of the workshop to *Stopped*. To make it *Ready* again, start the workshop: ```console $ workshop start ``` Both commands work gracefully, waiting for the workshop to comply: - **workshop stop** doesn’t destroy the workshop, unlike [remove](https://ubuntu.com/workshop/docs//tutorial/part-3-sketch-sdks.md#tut-remove) - **workshop start** doesn’t build it from scratch, unlike [launch](#tut-launch) or [refresh](#tut-refresh) In the next step, you’ll refresh an existing workshop. #### NOTE If issues arise now or later, see these guides: [How to troubleshoot Workshop](https://ubuntu.com/workshop/docs//how-to/fix-workshops/fix-installation.md#how-troubleshoot) and [How to debug issues in workshops](https://ubuntu.com/workshop/docs//how-to/fix-workshops/debug-issues.md#how-debug-issues-workshops). #### NOTE Consider adding the `.workshop.lock` file to your `.gitignore` or similar ignore files: ```console $ echo ".workshop.lock" >> .gitignore ``` In contrast, the `.workshop/` directory, which holds your definition, is *meant* to be stored in a repository; if your `.gitignore` file uses rules such as “ignore everything except these files and directories,” add them to the list of explicitly tracked items. ## Refresh a workshop Sometimes the base or the SDKs listed in your [workshop definition](#tut-define) are updated by their publishers. Alternatively, you may have changed the definition to switch bases, add and remove SDKs, or toggle their channels. A good example is when a new Ubuntu LTS version is released and, as a result, a new base image becomes available. In either case, you must refresh the workshop to apply the updates. For example, change the base and the SDK channel in your definition and refresh the workshop: ```yaml name: dev base: ubuntu@24.04 sdks: - name: ollama channel: vulkan/stable ``` ```console $ workshop refresh ``` After the refresh, **sdk list** may show multiple revisions of `ollama` if the previous `cpu/stable` volume is still on disk alongside the freshly pulled `vulkan/stable` one. Running **workshop refresh** is similar to a [launch](#tut-launch). However, it ensures the workshop remains operational. If issues occur, a refresh rolls back to a previous stable condition, whereas a failed launch has no condition to revert to and just fails. SDKs in a workshop diagnose themselves during launch and refresh; if an SDK fails to set up, the entire change is rolled back to keep the workshop operational. Finally, to discard **any changes made inside the workshop** since the last successful launch or refresh, run **workshop restore**. Unlike the automatic rollback on a failed refresh, this is a deliberate action that reverts the workshop to the most recent snapshot and resets it to its default state. Now that you can launch, refresh, start and stop a workshop, let’s move on to more practical purposes. ## Execute commands When the workshop is *Ready*, you can run arbitrary commands in it. In this tutorial, we’re working with Ollama AI models, and we’ve already created a Python project directory to serve as our workspace. First, let’s put our example workshop to practical use; download a simple AI model *inside the workshop* using the **workshop exec** command. We’ll use the `tinyllama` model, which is small and quick to download: ```console $ workshop exec dev -- ollama run tinyllama ``` This downloads and then runs the `tinyllama` model. The model will be stored in the mounted `models/` directory, so it persists between workshop refreshes. Quit the Ollama console by pressing `Ctrl+D`. You can also list the available models: ```console $ workshop exec dev -- ollama list NAME ID SIZE MODIFIED tinyllama:latest 2644915ede35 637 MB 24 seconds ago ``` Furthermore, your work files and deliverables, however complex they may be, can reside on the host system, while the toolchain is transparently confined and managed by **Workshop**. This enables you to focus on your project, switching when needed between language and framework versions or base images. Next, we’ll explore the remaining aspects of your daily workshop usage. #### NOTE **Workshop** also integrates with modern IDEs. For instance, see these guides: [How to connect your local VS Code to a workshop](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/connect-vscode.md#how-vscode-connect-remote). ### Interactive shell Besides running individual commands, you can open an interactive shell if you need to perform multiple operations within a session. **Workshop** runs the login shell for the default nonprivileged user, who’s also named `workshop`: ```console $ workshop shell workshop@dev-6b79e889:/project$ pwd /project workshop@dev-6b79e889:/project$ lsb_release -a ... Distributor ID: Ubuntu Description: Ubuntu 24.04.3 LTS Release: 24.04 Codename: noble workshop@dev-6b79e889:/project$ exit ``` ### Reusable actions For complex commands that you run often, define actions in your workshop definition to invoke them with **workshop run**. Add an `actions` section to `.workshop/dev.yaml`: ```yaml name: dev base: ubuntu@24.04 sdks: - name: ollama channel: vulkan/stable actions: pull: ollama pull "$@" ``` The `"$@"` expansion forwards every argument supplied after the action name into the action’s **bash** script. Pull a model through the new action; `"$@"` forwards `tinyllama` into **ollama pull**: ```console $ workshop run dev -- pull tinyllama ``` Finally, actions are parsed on every **workshop run**, so there’s no need to refresh the workshop after editing them. #### NOTE See [How to add actions to your workshop](https://ubuntu.com/workshop/docs//how-to/customize-workshops/add-actions.md#how-add-actions) for more on writing and running actions. ### Project directory updates Remember that the project directory is mounted as `/project/` when the workshop is launched; any changes to `/project/` from inside the workshop are visible in the project directory, and vice versa: ```console $ touch created_outside.txt $ workshop exec dev -- ls /project/ ... created_outside.txt ... $ workshop exec dev -- touch /project/created_inside.txt $ ls ... created_inside.txt created_outside.txt ... ``` Next, let’s dive into how changes and tasks work to track your workshop activities. ## Track changes and tasks To see how **Workshop** keeps track of its activities around a project, check out the recent major operations, or changes, with **workshop changes**: ```console $ workshop changes ID STATUS SPAWN READY SUMMARY 1 Done today at 09:26 CET today at 09:27 CET Launch "dev" workshop ... 4 Done today at 09:32 CET today at 09:34 CET Refresh "dev" workshop ``` Changes are enacted atomically to ensure workshops stay operational. Any change must have all its smaller steps, or tasks, succeed; otherwise, it will be reverted. To look at the latest change, run the **workshop tasks** command without an argument. To find out which tasks went into a certain change, pass the change ID to the command: ```console $ workshop tasks 4 STATUS DURATION SUMMARY Done 2m17.389s Download "ubuntu@24.04" base image Done 113ms Retrieve "system" SDK Done 2m59.777s Retrieve "ollama" SDK from channel "vulkan/stable" Done 443ms Create SDK state storage Done 581ms Run hook "save-state" for "system" SDK Done 449ms Run hook "save-state" for "ollama" SDK Done 54ms Disconnect interfaces of "ollama" SDK ... Done 528ms Setup "system" SDK profile ``` This lists all the tasks and includes logs for some of them; each task expresses a simple token of logic, such as running a hook or connecting an interface. ## Next steps This was the last step in this tutorial section; you are now familiar with the essential operations provided by **Workshop** and have had your first taste of what it can do for you. Your next step is to learn how to work with interfaces; proceed to the [Work with interfaces](https://ubuntu.com/workshop/docs//tutorial/part-2-work-with-interfaces.md#tut-work-with-interfaces) section. # part-2-work-with-interfaces.md # Work with interfaces This is the second section of the [four-part series](https://ubuntu.com/workshop/docs//tutorial/index.md#tut-index); it explains how to work with interfaces. It relies on the knowledge gained in the [Get started with workshops](https://ubuntu.com/workshop/docs//tutorial/part-1-get-started.md#tut-get-started) section, where you learned how to create and run workshops. Here, you will learn how to make better use of SDKs in your workshop and integrate them with the host system. SDKs use interfaces to interact in an organized manner, exposing the resources they provide via slots and consuming them via plugs; the layout of these plugs and slots is defined by the SDK publishers. For uniformity, security, and control, various host system capabilities (camera, GPU, and so on) are also exposed to the workshop via the same interface mechanism with a designated system SDK. ## Manage connections To check out the connected interfaces of a workshop, list the connections: ```console $ workshop connections INTERFACE PLUG SLOT NOTES gpu dev/ollama:gpu dev/system:gpu - mount dev/ollama:models dev/system:mount - ``` This lists two interface plugs, both provided by the `ollama` SDK under the `dev` workshop. The first one is a GPU interface plug named `dev/ollama:gpu`. It enables the workshop to use the host system’s GPU by connecting to the `dev/system:gpu` slot. Also, there’s a mount interface plug named `dev/ollama:models`. As seen in the **workshop info** output, it was automatically connected at [launch](https://ubuntu.com/workshop/docs//tutorial/part-1-get-started.md#tut-launch) to the `dev/system:mount` slot, indicated by the ellipsis in the `host-source` path. Note that some interfaces are auto-connected, while some are not; this depends on their built-in security policy defined by **Workshop**. For instance, you can’t use the ssh-agent interface without connecting it manually. In any case, you can connect and disconnect interfaces at will. To check the connection state, run **workshop connections**: ```console $ workshop disconnect dev/ollama:models $ workshop connections INTERFACE PLUG SLOT NOTES gpu dev/ollama:gpu dev/system:gpu - $ workshop connect dev/ollama:models :mount $ workshop connections INTERFACE PLUG SLOT NOTES gpu dev/ollama:gpu dev/system:gpu - mount dev/ollama:models dev/system:mount manual ``` You can remount a mount interface plug to a new location on the host. For example, to preserve models, conventionally stored under `~/.ollama/models/`, after the workshop is removed or use some models downloaded previously, you can remount to a directory in your home: ```console $ mkdir -p ~/.ollama/models $ workshop remount dev/ollama:models ~/.ollama/models $ workshop info name: dev base: ubuntu@24.04 project: /home/user/ollama-python-project status: ready notes: - sdks: system: installed: (1) ollama: tracking: vulkan/stable installed: 0.9.6 2025-11-19 (214) mounts: models: host-source: /home/user/.ollama/models workshop-target: /home/workshop/.ollama/models ``` This makes `/home/user/.ollama/models/` on the host act as the models storage for the workshop. ## Add plugs, slots You can modify the behavior of the SDKs you installed in your workshop, tailoring it to your needs and connecting them to other SDKs or the host system. To do this, you add plugs and slots to the SDKs in the workshop definition, allowing you to customize the initial plug and slot layout to your requirements. This scenario usually arises when you want to connect different SDKs running in the workshop or expose some service from the workshop to the host system. Let’s look at an example. Add the `jupyter` SDK to the workshop to run Jupyter notebooks with the Ollama models: ```yaml name: dev base: ubuntu@24.04 sdks: - name: ollama channel: vulkan/stable - name: jupyter ``` ```console $ workshop refresh "dev" refreshed ``` Next, add the tunnel interface plug under the `system` SDK in the workshop definition; this exposes the Jupyter server, now available in the workshop, to the host system at a port of your choice (here, `8989`): ```yaml name: dev base: ubuntu@24.04 sdks: - name: ollama channel: vulkan/stable - name: jupyter - name: system plugs: jupyter: interface: tunnel endpoint: 127.0.0.1:8989 ``` The slot we’re going to connect this plug to comes from the SDK itself and is named `jupyter`, so you don’t have to add it manually: ```console $ workshop connections --all INTERFACE PLUG SLOT NOTES gpu dev/ollama:gpu dev/system:gpu - mount dev/jupyter:venv dev/system:mount - mount dev/ollama:models dev/system:mount - tunnel - dev/ollama:ollama-server - tunnel dev/system:jupyter dev/jupyter:jupyter - ``` Refresh the workshop to enable the tunnel; **Workshop** will auto-connect the plug to the slot by matching their names (the plug’s name is also `jupyter`). Check the result using **workshop info**: ```console $ workshop refresh "dev" refreshed $ workshop info ... sdks: system: installed: (1) tunnels: jupyter: from: 127.0.0.1:8989/tcp to: 127.0.0.1:8888/tcp ... ``` Now, JupyterLab is available at the plug address: ```console $ curl -w '\n' http://127.0.0.1:8989/api {"version": "2.17.0"} ``` #### NOTE For additional details of using the tunnel interface, see this guide: [How to forward ports with tunneling](https://ubuntu.com/workshop/docs//how-to/customize-workshops/forward-ports.md#how-forward-ports). ## Wire jupyter to a uv-managed Python environment So far, `jupyter:venv` auto-connects to the `system:mount` slot, which gives Jupyter a private host directory for its virtual environment. A more interesting wiring uses the `uv` SDK, the standard Python tooling SDK in **Workshop**; `uv` exposes a `venv` slot that other Python-based SDKs can plug into, so Jupyter and uv share a single environment. Edit the workshop definition to add `uv` *before* `jupyter` in the `sdks:` list, so that `uv`’s `setup-project` hook prepares the shared virtual environment before any consuming SDK installs into it. Then declare the connection in a top-level `connections:` block: ```yaml name: dev base: ubuntu@24.04 sdks: - name: ollama channel: vulkan/stable - name: uv - name: jupyter - name: system plugs: jupyter: interface: tunnel endpoint: 127.0.0.1:8989 connections: - plug: jupyter:venv slot: uv:venv ``` Apply the new definition by refreshing the workshop: ```console $ workshop refresh ``` `dev/jupyter:venv` now connects to `dev/uv:venv` instead of falling back to `dev/system:mount`: ```console $ workshop connections --all INTERFACE PLUG SLOT NOTES gpu dev/ollama:gpu dev/system:gpu - mount dev/jupyter:venv dev/uv:venv - mount dev/ollama:models dev/system:mount - mount dev/uv:cache dev/system:mount - tunnel - dev/ollama:ollama-server - tunnel dev/system:jupyter dev/jupyter:jupyter - ``` This is your first taste of slot/plug coordination between two nonsystem SDKs; for the full Python workflow with **uv**, see [How to manage Python environments with the uv SDK](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/manage-python-environments.md#how-manage-python-environments). ## Next steps This was the last step in this tutorial section; you’re halfway through! Now you are familiar with the essentials of interfaces in **Workshop**. Your next step is to learn even more about workshop customization, creating experimental SDKs quickly with the **workshop sketch-sdk** command; proceed to the [Customize with sketch SDKs](https://ubuntu.com/workshop/docs//tutorial/part-3-sketch-sdks.md#tut-sketch-sdks) section. # part-3-sketch-sdks.md # Customize with sketch SDKs This is the third section of the [four-part series](https://ubuntu.com/workshop/docs//tutorial/index.md#tut-index); it teaches you to create experimental SDKs quickly using the **workshop sketch-sdk** command to run local SDK experiments without publishing them. It relies on the knowledge gained in the [Get started with workshops](https://ubuntu.com/workshop/docs//tutorial/part-1-get-started.md#tut-get-started) section, where you learned how to create and run workshops. Suppose you built your workshop with a number of SDKs only to realize it still lacks some functionality you need. Naturally, you’d like to add that, but can you align it with the way **Workshop** operates? Fortunately, **Workshop** allows you to quickly draft a local SDK and use it within your workshop. This process is called *sketching*. #### NOTE For details on how sketch SDKs are different from regular SDKs, see the [Sketch SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sketch-sdk) explanation section. ## Introduction We’ll use the following scenario to demonstrate how to iterate on an SDK to add missing functionality. Suppose you’re running the `dev` workshop from the [Get started with workshops](https://ubuntu.com/workshop/docs//tutorial/part-1-get-started.md#tut-get-started) tutorial section, additionally augmented with the `jupyter` SDK when we discussed [Work with interfaces](https://ubuntu.com/workshop/docs//tutorial/part-2-work-with-interfaces.md#tut-work-with-interfaces): ```yaml name: dev base: ubuntu@24.04 sdks: - name: ollama channel: vulkan/stable - name: jupyter - name: system plugs: jupyter: interface: tunnel endpoint: 127.0.0.1:8989 ``` In our example workshop, this setup allows you to work with AI models using Ollama and Jupyter. But what if the SDKs in your workshop don’t provide some tools? For instance, you may have a HuggingFace SDK without **huggingface-cli**. Should you create and publish an SDK just for your personal setup? Probably not. In this guide, we’ll add [jupyter-console](https://jupyter-console.readthedocs.io/en/latest/), an interactive Python environment that can run notebook-style code directly in the terminal. Let’s explore how to integrate this utility into your workshop in a way that aligns with **Workshop**. ## Start sketching Instead of manually installing tools using **workshop shell** or **workshop exec**, you can create a local SDK that automates these tasks with **Workshop**. Running **workshop sketch-sdk** opens a simplified version of an [SDK definition](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-definition). This defines all SDK components in a single file named `sdk.yaml`: ```console $ workshop sketch-sdk ``` The editor presents a minimal setup with empty `hooks`, `plugs`, and `slots`: ```yaml name: sketch hooks: # ... plugs: # ... slots: # ... ``` Under `hooks`, you’ll find a commented `setup-project` section. It runs as the `workshop` user after the project directory is mounted and interfaces are connected. Normally, you use it for commands that access the project files or simply should not run as root, so add the following to install Jupyter Console: ```yaml name: sketch hooks: setup-project: | source /var/lib/workshop/sdk/jupyter/venv/bin/activate pip install jupyter-console ``` This uses the existing Jupyter virtual environment, created by the `jupyter` SDK to install the `jupyter-console` package. Here, the path follows a certain structure: `/var/lib/workshop/sdk/` is the location of all SDKs inside a workshop, `jupyter/` is the specific SDK name, and `venv/` is a directory specific to the SDK. Once you save and exit `sdk.yaml`, **Workshop** refreshes the workshop, running the new hooks: ```console ... Run hook "setup-project" for "sketch" SDK ... "dev" sketch refreshed ``` If errors occur, you can debug the sketch SDK like any other, using **workshop changes**, **workshop tasks**, and **workshop refresh** with `--continue` or `--abort`. For help, see [this guide](https://ubuntu.com/workshop/docs//how-to/fix-workshops/debug-issues.md#how-debug-issues-workshops). Note that aborting the refresh does not revert your sketched changes, so you can always restart where you left off by running **workshop sketch-sdk** again. After the refresh, the output of **workshop info** should include something like this: ```console $ workshop info ... sketch: tracking: ~/.local/share/workshop/id/6b79e889/dev/sdk/sketch/current installed: 2025-08-27 (x1) ``` The sketch SDK entry shows the last update time and its revision (`x1`). The SDK is local, so `tracking` lists the SDK definition path on the host; each edit with **workshop sketch-sdk** bumps the revision number. At this point, you’ve created a functional, albeit simple, SDK in minutes. Now you can use Jupyter Console to interactively work with your Ollama models. Start the Jupyter Console by activating the virtual environment provided by the `jupyter` SDK and using the **jupyter console** command enabled by the sketch SDK: ```console $ workshop shell workshop@dev-6b79e889:/project$ source /var/lib/workshop/sdk/jupyter/venv/bin/activate (jupyter-venv) workshop@dev-6b79e889:/project$ jupyter console Jupyter console 6.6.3 ... ``` This opens an interactive environment where you can experiment with Ollama. Try it out by running some Python code to interact with your models. Install some dependencies first, as you would normally do with Jupyter: ```console In [1]: %pip install requests ``` Then execute the following code to test the Ollama API: ```python import requests # Check if Ollama is running response = requests.get('http://localhost:11434/api/version') print(f"Ollama version: {response.json()['version']}") # Generate text with the tinyllama model, installed in Part 1 data = { "model": "tinyllama", "prompt": "Why is the sky blue?", "stream": False } response = requests.post('http://localhost:11434/api/generate', json=data) print(response.json()['response']) ``` If everything is set up correctly, you should see the Ollama version and a response to the prompt: ```console Ollama version: 0.9.6 The sky is blue due to its light absorption by the air and water molecules. The atmosphere contains small amounts of carbon dioxide, which helps absorb more blue-violet light from the sun. The amount of red light absorbed also plays a role in determining the color of the sky. The exact combination of these factors can vary between different regions around the world due to changes in climate and topography. Additionally, some cultures have myths or beliefs about what colors are associated with various things, such as green for growth, blue for water, etc. ``` Quit the Jupyter console and the workshop shell by pressing `Ctrl+D` twice. If you need to make more changes or experiment, just run **workshop sketch-sdk** again to update your sketch SDK. Repeat this as often as needed until it works the way you want. #### NOTE The **workshop sketch-sdk** command opens the SDK definition in your default text editor. To use a specific editor, set the `EDITOR` environment variable, e.g.: ```console $ export EDITOR=vim $ workshop sketch-sdk ``` #### NOTE For more details on SDK definition components, see the [explanation](https://ubuntu.com/workshop/docs//explanation/index.md#exp-index) section. You may want to start with [SDKs](https://ubuntu.com/workshop/docs//explanation/index.md#exp-sdks) and [Interfaces](https://ubuntu.com/workshop/docs//explanation/index.md#exp-interfaces). ## Stash and restore You can temporarily stash the sketch SDK to revert your workshop to its presketching state: ```console $ workshop sketch-sdk --stash $ workshop info ``` To restore the stashed SDK: ```console $ workshop sketch-sdk --restore ``` #### WARNING Stashing does not delete the SDK, allowing you to restore it and continue working later. However, there’s only one slot available for stashing. Running **workshop sketch-sdk** overwrites the existing stash, if any. Be cautious to avoid losing your changes. ## Explore sketches You can only have one sketch SDK per workshop at a time; there’s no way to add `sketch-foo`, `sketch-draft`, `sketch-final-final`, and so on. However, a project may contain multiple workshops, each with its own sketch SDK. To explore the available sketches in your project and their respective states, use the **workshop sketches** command: ```console $ workshop sketches ``` ## Convert to in-project SDK If you’re happy with your sketch SDK, your first option is to convert it into an [in-project SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-in-project-sdk). This makes it a permanent, version-controllable part of your project, shareable with your team; a good step before deciding to publish it to the SDK Store for wider use. To convert the sketch, you *eject* it with the `--eject` option. This creates a new in-project SDK by moving the sketch’s definition files into the `.workshop/` subdirectory of your project. The original sketch SDK is removed from the workshop. **Workshop** can then pull the SDK directly from this directory, bypassing the SDK Store. By default, the new SDK is named after the project directory; to change this, use the `--name` option: ```console $ workshop sketch-sdk --eject --name console "dev" sketch ejected to ".workshop/console" To use it, add "project-console" to the list of SDKs and run 'workshop refresh dev' ``` After ejecting, add the new in-project SDK to your workshop definition (in `.workshop/dev.yaml`) under the `sdks:` list, using the `project-` prefix so **Workshop** knows it’s an in-project SDK and looks for it in the `.workshop/` directory: ```yaml sdks: - name: project-console ``` Next, run **workshop refresh** to apply the change. If everything is set up correctly, it’s time to preserve the changes. The definition and hooks of the newly ejected `console` SDK are placed in the `.workshop/console/` subdirectory: ```console .workshop/console/ ├── hooks │   └── setup-project └── sdk.yaml ``` If your project did not previously have a `.workshop/` directory, add its contents to version control: ```console $ git add .workshop/ $ git commit -m "Add jupyter-console in-project SDK" ``` This ensures your in-project SDK is tracked and can be shared with collaborators or CI systems. #### NOTE For a detailed comparison of in-project SDKs with other SDK types, see the [In-project SDKs](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-in-project-sdk) explanation section. If you intend to publish a regular SDK, proceed to the next part of the tutorial, [Craft SDKs with SDKcraft](https://ubuntu.com/workshop/docs//tutorial/part-4-craft-sdks.md#tut-craft-sdks). ## Clean up If you’re not quite satisfied with your sketching experiments, your second option is to remove the sketch SDK permanently: ```console $ workshop sketch-sdk --remove ``` This deletes all changes introduced by the sketch. Also, note that **workshop remove** removes the sketch SDK, as you could expect, including its stashed version. ## Remove a workshop When you’re done with sketching, the only thing left to cover for local workshops is the cleanup. If you no longer need your workshop, remove it: ```console $ workshop remove ``` This doesn’t affect the files in the project directory, including the workshop definition, or any other content that was stored outside the workshop (e.g., using the [mount interface](https://ubuntu.com/workshop/docs//tutorial/part-2-work-with-interfaces.md#tut-interfaces) with a custom **workshop remount** location; however, the content in *default* mount locations will be deleted). Even if you remove the workshop completely, you can rebuild it with **workshop launch**; this may come in handy if you have removed your workshop using the command above before proceeding to the other parts of the tutorial. #### WARNING Don’t delete the project directory without first removing the workshop. Otherwise, you’ll need to manually delete the orphaned workshops; for help, see this how-to guide section: [Explore LXD containers](https://ubuntu.com/workshop/docs//how-to/fix-workshops/fix-installation.md#how-troubleshoot-lxc). ## Next steps This was the last step in this tutorial section; you are now familiar with the essentials of building SDKs in **Workshop** and have had your first taste of what sketching can achieve. If you’ve mastered local SDKs, your next step is to start creating publicly available SDKs; proceed to the [Craft SDKs with SDKcraft](https://ubuntu.com/workshop/docs//tutorial/part-4-craft-sdks.md#tut-craft-sdks) section. # part-4-craft-sdks.md # Craft SDKs with **SDKcraft** This is the fourth section of the [four-part series](https://ubuntu.com/workshop/docs//tutorial/index.md#tut-index); you’ll learn how to create full-featured SDKs that can be published and shared with others using **SDKcraft**. It relies on the knowledge gained in the [Get started with workshops](https://ubuntu.com/workshop/docs//tutorial/part-1-get-started.md#tut-get-started) section, where you learned how to create and run workshops, and also builds on the [Customize with sketch SDKs](https://ubuntu.com/workshop/docs//tutorial/part-3-sketch-sdks.md#tut-sketch-sdks) section, where you learned how to sketch local SDKs. Here, you will initialize, define, pack, and publish an [SDK](https://ubuntu.com/workshop/docs//explanation/index.md#exp-sdks): a set of hooks, interfaces, and parts that is bundled into a single package, suitable for use with **SDKcraft**, the user-oriented CLI utility. The commands you’re about to run cover most of your daily needs with **SDKcraft**. ## Install **SDKcraft** Install the snap using the [--classic](https://snapcraft.io/docs/install-modes/) option: ```console $ sudo snap install --classic sdkcraft ``` ### Prerequisites **SDKcraft** relies on [LXD 6.8+](https://canonical.com/lxd) for low-level operation, using its [REST API](https://documentation.ubuntu.com/lxd/latest/restapi_landing/) to craft the SDKs. If the **snap install** command reports an issue with LXD, install a recent LXD version with **snap**. To install it from scratch: ```console $ sudo snap install --channel=6/stable lxd ``` To refresh an existing installation: ```console $ sudo snap refresh --channel=6/stable lxd ``` #### NOTE For other ways to install LXD, see the available installation options in [LXD documentation](https://documentation.ubuntu.com/lxd/latest/installing/). Also, you need to ensure the [LXD daemon](https://documentation.ubuntu.com/lxd/latest/explanation/lxd_lxc/#lxd-daemon) is enabled and running. Again, refer to LXD documentation and your distribution’s manuals for guidance. ## Initialize the SDK Once you have installed **SDKcraft**, use it to initialize, define, and pack your first [SDK](https://ubuntu.com/workshop/docs//explanation/index.md#exp-sdks). Here, we’ll build an SDK that installs Ollama for running large language models in the workshop. This demonstrates creating an SDK for a specific application, but SDKs can package any software that aligns with the **Workshop** way. First, create a directory named `ollama/`: ```console $ mkdir ollama/ ``` It will contain your [SDK definition](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-definition) and other source files. Next, browse to the SDK directory and initialize it: ```console $ cd ollama/ $ sdkcraft init ``` This command creates a template definition file named `sdkcraft.yaml`; although it’s almost empty, it can already be [built](#tut-sdkcraft-try). However, let’s take a few extra steps to explore what goes into an SDK. ## Update metadata Update the metadata in `sdkcraft.yaml` with some domain-specific information to describe the project and build SDKs for several platforms: ```yaml name: ollama version: "0.9.6" summary: Get up and running with large language models description: | Get up and running with Llama 3.3, DeepSeek-R1, Phi-4, Gemma 3, Mistral Small 3.1 and other large language models. license: MIT platforms: ubuntu@22.04:amd64: ubuntu@24.04:amd64: parts: my-part: plugin: nil ``` ## Define parts **SDKcraft** leverages the [parts mechanism](https://ubuntu.com/workshop/docs//explanation/sdks/parts.md#exp-sdk-parts) to obtain data from different sources, process it in various ways, and prepare an SDK package for publishing. In our Ollama SDK, we’ll define two parts: one to download the Ollama binary from its GitHub release page, and another for the **systemd** service file: ```yaml # ... parts: ollama: plugin: dump source: https://github.com/ollama/ollama/releases/download/v${CRAFT_PROJECT_VERSION}/ollama-linux-amd64.tgz source-type: tar user-service: plugin: dump source: ollama.service source-type: file ``` The `ollama` part uses the `dump` plugin to download and extract the official Ollama binary from GitHub releases. The `user-service` part includes a **systemd** service file that will be used to manage the Ollama daemon. The first `dump` downloads the file automatically. However, we need to create the **systemd** service file that was referenced in the `user-service` part. In the `ollama/` directory, create a file named `ollama.service`: ```ini [Unit] Description=Ollama Service After=network.target [Service] ExecStart=/bin/bash -lc "ollama serve" Restart=always RestartSec=3 [Install] WantedBy=default.target ``` This defines how the Ollama daemon should run: - `ExecStart` starts the server with a login shell to pick up the environment - `Restart` ensures the service is restarted on failure - `After` makes it depend on network connectivity #### NOTE The service file is specific to Ollama and how it runs as a daemon. This is just one way to manage a long-running process in an SDK, and other SDKs may use different part layouts depending on their needs. For in-depth details, refer to the [Parts](https://documentation.ubuntu.com/craft-parts/latest/common/craft-parts/explanation/parts/) section in Craft Parts documentation. ## Add plugs and slots In **SDKcraft**, [interfaces](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-concepts) provide a controllable way of exposing the resources of the host system to the workshops, and you can use them in a variety of ways to extend the functionality of your SDK. For the Ollama SDK, we need several interfaces: a mount interface to preserve models, a GPU interface for acceleration, and a tunnel interface to expose the API server. The latter is a resource that the SDK itself exposes, so it will be defined as a slot; the former two are plugs because they access external resources. Open `sdkcraft.yaml` again and add two plugs and a slot to the appropriate sections: ```yaml name: ollama version: "0.9.6" summary: Get up and running with large language models description: | Get up and running with Llama 3.3, DeepSeek-R1, Phi-4, Gemma 3, Mistral Small 3.1 and other large language models. license: MIT platforms: ubuntu@22.04:amd64: ubuntu@24.04:amd64: plugs: gpu: interface: gpu models: interface: mount workshop-target: /home/workshop/.ollama/models slots: ollama-server: interface: tunnel endpoint: 11434 # ... ``` The `models` plug preserves downloaded models between workshop refreshes. The `gpu` plug provides access to GPU acceleration for faster inference, and the `ollama-server` slot exposes the Ollama API on port 11434. #### NOTE You can’t explicitly set the *host* directory for mount plugs here; this restriction prevents SDKs from accessing any arbitrary data on the host filesystem. However, users who add your SDK to their workshops will be able to remount the plug elsewhere at runtime. ## Add hooks To prepare the SDK for use, add the [hooks](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks) that run at different stages of the workshop’s lifecycle, preparing the SDK for use or preserving its state during updates. Under `ollama/`, there is a subdirectory named `hooks/`. This directory stores all the hooks for an SDK. ### Build: setup base, project Under `ollama/hooks/`, edit the file named `setup-base`: ```shell cat </etc/profile.d/ollama.sh export PATH="$SDK/bin:\$PATH" EOF ``` It runs when the workshop is launched or refreshed, and is typically used to install system packages and configure the environment. In the same directory, edit the file named `setup-project` for Ollama-specific setup: ```shell install -D --mode=644 --target-directory ~/.config/systemd/user "$SDK/ollama.service" systemctl --user daemon-reload systemctl --user enable --now ollama ``` It runs after `setup-base`, once the project directory is mounted and interfaces are connected. This hook installs and starts the Ollama service as a user service, ensuring the AI model server is running and ready to use. #### NOTE **Workshop** tweaks this hook’s environment a bit. First, note the `$SDK` variable, which points to the root of the SDK installation. This allows you to reference files installed by the SDK. Also, when invoked from any hooks, **apt** is configured to exclude recommended or suggested packages and answer “yes” to all confirmation prompts. ### Persist: save and restore Some SDKs need to preserve internal state during workshop refresh operations, such as configuration settings or temporary data that shouldn’t be lost. For these cases, you would create `save-state` and `restore-state` hooks. During a **workshop refresh** operation: - The `save-state` hook runs *before* the workshop is refreshed, saving the state of the SDK to `$SDK_STATE_DIR`. - The `restore-state` hook recovers the state *after* the workshop has been successfully updated. However, the Ollama SDK doesn’t need these hooks because: - Downloaded models are stored in the mounted `models/` directory, which persists across refreshes - The **systemd** service configuration is stateless and recreated on each refresh - No custom user configuration needs to be preserved #### WARNING The SDK is also refreshed as a part of any workshop refresh operation, so any breaking changes in its save-restore logic will cause an error; make sure to allow for this in your SDK design. ### Report: check health Finally, create a hook named `check-health` to test whether the installation is functional and report to **Workshop** accordingly: ```shell if ! output=$(sudo -u workshop --login ollama list 2>&1); then workshopctl set-health error "$output" exit fi workshopctl set-health okay ``` It checks whether the Ollama installation is functional by running **ollama list**. If it succeeds, the health is set to `okay` using the [workshopctl set-health](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-workshopctl) command; otherwise, it reports the error output from the failed command. In general, the hook should set the health to `okay` and return a zero code if its health checks succeed. To signal an error, set the health to `error` or return a nonzero code. You can also set the health to `waiting` to signal that the hook should be retried for a few seconds. Unless the hook sets the health to a different value during such a retry, the health is eventually set to `error` automatically. #### NOTE The use of **sudo -u workshop** here is important because only the `setup-project` hook runs as a normal user by default; other hooks, like `check-health`, run as root. Running commands as the nonroot user helps preserve the correct environment variables and file ownership, and can be easier than adjusting permissions afterwards. ## Try the SDK When you’re confident the SDK is ready to be built, try it in-place before uploading it to the Store. Under `ollama/`, run: ```console $ sdkcraft try Packed ollama_amd64_ubuntu@22.04.sdk Packed ollama_amd64_ubuntu@24.04.sdk ... ``` Optionally, you can clean the build cache before trying: ```console $ sdkcraft clean && sdkcraft try ``` The command builds and packs the SDK into files such as `ollama_amd64_ubuntu@24.04.sdk`, which contain the build artifacts along with SDK metadata, hooks, and other components. This is repeated for all supported `platforms` defined in the `sdkcraft.yaml` metadata. In particular, the command builds all [SDK parts](https://ubuntu.com/workshop/docs//explanation/sdks/parts.md#exp-sdk-parts) defined in the `sdkcraft.yaml` file, e.g., pulling source code, applying patches, configuring and compiling it according to the part definition. After a successful build, the **sdkcraft try** command also copies the SDKs to a special *try area* (usually `$XDG_DATA_HOME/workshop/try/`). To use them in a workshop, add a prefix: `try-`: ```yaml name: dev base: ubuntu@24.04 sdks: - name: try-ollama ``` A `channel` is not needed here; the SDK is installed from the try area when you launch the workshop; the options `--verbose` and `--wait-on-error` help debug any issues that may arise during launch or refresh: ```console $ workshop launch --verbose --wait-on-error ``` #### NOTE For a detailed explanation of the build process, see the respective Craft Parts [documentation section](https://documentation.ubuntu.com/craft-parts/latest/common/craft-parts/explanation/lifecycle/). ## Test the SDK Additionally, you can write and run [spread](https://github.com/canonical/spread) tests against the SDK to ensure its functionality and catch any issues before publishing it. For **SDKcraft**, **spread** tests are declared under `tests/` in the SDK directory; each test describes a specific executable user workflow. To run the test suite against the packed SDK: ```console $ sdkcraft test ``` At runtime, each test provisions a clean LXD container, installs the packed SDK into a workshop, and runs the declared scenarios end-to-end. ## Publish the SDK When an SDK is ready, built, and tried, publish it to the SDK Store for use with **Workshop**. Authenticate, register the SDK name, and upload the artifact: ```console $ sdkcraft login $ sdkcraft register ollama $ sdkcraft upload ./ollama_amd64_ubuntu@24.04.sdk --release latest/beta ``` This uploads the newly created SDK and releases it under the `latest/beta` channel in the SDK Store. For the full publish flow, including how to release already-uploaded revisions to additional channels, see [How to publish an SDK](https://ubuntu.com/workshop/docs//how-to/develop-sdks/publish-an-sdk.md#how-publish-sdk). ## Use the SDK The resulting SDK can be used with **Workshop** as follows: ```yaml name: dev base: ubuntu@24.04 sdks: - name: ollama channel: latest/beta ``` Note that the workshop `base` must match one of the SDK’s supported `platforms`. ## Summary This was the last step of the entire tutorial series. You have learned how to create a workshop, add SDKs to it, and use them in practice. You have also learned how to sketch a local SDK and how to craft and publish a full-featured SDK. You are now familiar with all the basic operations that **Workshop** and **SDKcraft** provide and have had an extensive tour of their capabilities. # parts.md # SDK parts Parts provide a way to modularize the SDK and manage its dependencies, ultimately making it easier to maintain and update by separating its deployment into sourcing, building and staging phases. ## Summary Parts can be thought of as the building blocks of an SDK. Each part in the [definition](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-definition) encapsulates a different aspect of the SDK and focuses on a specific feature or resource; these can be libraries, binaries, or configuration files. A part defines a number of preset attributes and lifecycle stages in YAML; **SDKcraft** executes these definitions stage by stage and iteratively resolves any dependencies between parts. Eventually, this results in a uniform SDK, ready for publishing and installation; such SDKs arrive to the users prebuilt, allowing to factor out build activities from [SDK hooks](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks) that **Workshop** executes inside the workshop at runtime. ## Implementation notes Full disclosure: **SDKcraft** borrows the [Craft Parts](https://github.com/canonical/craft-parts/) mechanism from the upstream [Craft Application](https://github.com/canonical/craft-application/) project, ultimately sharing it with such tools as [Snapcraft](https://documentation.ubuntu.com/snapcraft/stable/) and [Charmcraft](https://documentation.ubuntu.com/charmcraft/stable/), so general approaches that work for any of those will apply here. Aside from not yet allowing `stage-packages` and `stage-snaps`, **SDKcraft** doesn’t further limit or expand the parts functionality. However, be aware of the requirements and limitations that the upstream project places on what’s available for a given base, plugin, source and so on. A detailed explanation is available in the corresponding Craft Parts [documentation section](https://documentation.ubuntu.com/craft-parts/latest/explanation/). ## See also Explanation: - [SDKs](https://ubuntu.com/workshop/docs//explanation/index.md#exp-sdks) Reference: - [SDK parts](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-sdk-parts) # projects.md # Projects Technically, a project is a directory containing one workshop definition or more. To initialize a directory as a project, create a [workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) in it and run **workshop launch**. Launching a workshop from a project establishes the relationship between the two that’s required to actually start a workshop. This is achieved with a hidden `.workshop.lock` file, which must remain in the project directory and must not be copied or stored externally, e.g., in a repository. You can store workshop definitions in two ways: - If you use a single workshop in the project, store its definition in the project directory as `workshop.yaml`. This allows you to omit the workshop name in [the CLI](https://ubuntu.com/workshop/docs//explanation/workshops/workshop-cli.md#exp-workshop-cli). - If your project involves multiple workshops, store their definitions in files with the same name as the workshops under the `.workshop/` subdirectory of the project directory: ```none .workshop/foo.yaml .workshop/bar.yaml ``` When multiple workshop definitions are present, you can’t omit the workshop name in commands. Projects can also store SDK definitions in subdirectories of `.workshop/`: ```none .workshop/build-tools/sdk.yaml .workshop/system-services/sdk.yaml ``` Such SDKs can be used by any workshop in the project. When a workshop is then started with **workshop start**, the project directory is mounted to it as `/project/`; conversely, the **workshop stop** command unmounts it. External changes to the project are tracked by the **Workshop** daemon. Thus, if the project is moved or copied, all workshops that reference it are updated, so you can continue working without interruption. If the project is deleted by external means without first removing its workshops, any workshops that reference it enter the *Error* state; the only command applicable to them is **workshop remove**. #### NOTE There are more workshop CLI commands; some have a `--project` option that accepts a pathname to use as the project directory. ## See also Explanation: - [Workshops and projects](https://ubuntu.com/workshop/docs//explanation/index.md#exp-workshop) - [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) How-to guides: - [How to move projects around](https://ubuntu.com/workshop/docs//how-to/customize-workshops/move-projects.md#how-move-projects) Reference: - [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) - [workshop remove](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-remove) - [workshop start](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-start) - [workshop stop](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-stop) # publish-an-sdk.md # How to publish an SDK Publishing turns a packed SDK into something other **Workshop** users can pull from the SDK Store. If the SDK isn’t yet packed, tested, and tried locally, go through [How to build an SDK](https://ubuntu.com/workshop/docs//how-to/develop-sdks/build-an-sdk.md#how-build-sdk) first. The publishing flow has four steps: 1. **Pack** the SDK into one `.sdk` artifact per platform. 2. **Register** the SDK name on the SDK Store. 3. **Upload** a revision. 4. **Release** the revision to one or more channels. The first step runs on your machine. The last three talk to the live SDK Store at `api.charmhub.io` and require an authenticated account. ## Prerequisites Before starting: - **SDKcraft** is installed. - LXD 6.6 or later is running on the host. - An Ubuntu One account. - The SDK source tree is clean and ready to build. - The SDK passes **sdkcraft try** end-to-end in at least one workshop. There is no local-only or dry-run mode for the Store-side commands. Plan to publish from a workstation with a stable network connection. ## Pack the SDK **sdkcraft pack** builds the SDK and packs it into one artifact per platform declared in `sdkcraft.yaml`: ```console $ sdkcraft pack ``` The resulting filenames follow the pattern `__.sdk`, for example `_amd64_ubuntu@24.04.sdk`. **sdkcraft pack** differs from **sdkcraft try** in one respect: the artifacts stay in the working directory rather than being copied into the [try area](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-test-try-sdk). If a previous build left state behind, clean and rebuild from scratch: ```console $ sdkcraft clean && sdkcraft pack ``` ## Test the SDK If the SDK ships a `tests/` directory with [spread](https://github.com/canonical/spread) tests, run them against the freshly packed artifacts: ```console $ sdkcraft test ``` **SDKcraft** provisions a clean LXD container for each test, installs the packed SDK into a workshop, and runs the declared scenarios end-to-end. **sdkcraft init** scaffolds a starter test under `tests/main/launch/` and a `tests/spread.yaml` declaring the suites that **sdkcraft test** should pick up. Add more tests next to the starter, each in its own subdirectory of the same suite: ```yaml summary: SDK installs and reports healthy execute: | workshop launch --verbose --wait-on-error workshop info | grep -E 'status:\s+okay' ``` ## Try the SDK The final pre-publish step is to install the packed SDK in a real workshop and use it the way an end user would: ```console $ sdkcraft try ``` **sdkcraft try** packs the SDK and copies it into the [try area](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-test-try-sdk). Add it to a workshop with the `try-` prefix: ```yaml name: dev base: ubuntu@24.04 sdks: - name: try- ``` Then launch the workshop and exercise the SDK: ```console $ workshop launch --verbose --wait-on-error ``` This is the last chance to catch problems before the SDK is on the Store. Pay particular attention to: - Hook output in **workshop changes** and **workshop tasks**. - The SDK’s `status` in **workshop info**; a `waiting` or `error` state is the SDK telling you something is wrong. - The interaction between this SDK and any other SDKs it’s meant to be installed alongside. ## Register the SDK name Each SDK on the Store has a unique name. Reserve yours once per SDK, ever. Any authenticated Ubuntu One account can register an available name and publish under it, much as anyone can register a new snap name. Once a name is registered, only the SDK’s publisher and collaborators can upload or release revisions to it. Authenticate first: ```console $ sdkcraft login ``` Confirm the right account is active: ```console $ sdkcraft whoami ``` Then register the SDK name: ```console $ sdkcraft register ``` Names are global to the SDK Store and normally cannot be re-registered after release. Pick a name that matches the SDK’s `name` field in `sdkcraft.yaml` and that you intend to keep. ## Upload a revision Each **sdkcraft upload** invocation pushes one `.sdk` file and assigns it a revision number on the Store: ```console $ sdkcraft upload _amd64_ubuntu@24.04.sdk ``` The output reports the revision number. At this point, the revision is on the Store but isn’t released to any channel yet, so **sdk find** won’t return it. To upload and release in one step, pass `--release` with one or more channels: ```console $ sdkcraft upload _amd64_ubuntu@24.04.sdk --release latest/edge ``` Upload one artifact per platform. If **sdkcraft pack** produced `_amd64_ubuntu@22.04.sdk` and `_amd64_ubuntu@24.04.sdk`, upload both; the Store tracks revisions per platform. ## Automate uploads from CI The `.github/workflows/` files that ship with the [canonical/template-sdk](https://github.com/canonical/template-sdk) repository run **sdkcraft upload --release** automatically on push to the version branch that `renovate.json` maintains. After the one-time **sdkcraft register**, upstream releases land as automated revisions without further manual commands: Renovate opens a PR bumping `VERSION`, the merge of that PR triggers the upload workflow, and the new revision shows up in the configured channels. The workflow expects Store credentials in the repository’s GitHub Actions secrets; configure them once. For what else the template ships, see [How to build an SDK](https://ubuntu.com/workshop/docs//how-to/develop-sdks/build-an-sdk.md#how-build-sdk). ## Release a revision When a revision is on the Store but not yet released, or when promoting an existing revision from one channel to another, use **sdkcraft release**: ```console $ sdkcraft release ``` Channels follow the `[/][/]` shape: - `` is optional and groups related revisions, typically along major-version lines or variations in supported platforms (for example, `1.x` or `nvidia`). Omitting it targets the default `latest` track. - `` is one of `stable`, `candidate`, `beta`, or `edge`. - `` is optional and creates a short-lived channel with a one-month expiration. Plain `stable` and comma-separated lists like `beta,edge` are valid channel arguments. For example, to promote revision 8 to `latest/stable`: ```console $ sdkcraft release 8 latest/stable ``` **sdkcraft release** is idempotent and never rebuilds or re-uploads; it only adjusts the channel map. ## Release to a non-default track Releasing to `latest` needs no setup; the `latest` track always exists. Releasing to any other track requires that the track exist first. The **sdkcraft create-track** command creates one: ```console $ sdkcraft create-track --track 1.x ``` It only creates track names that the SDK Store already permits through a *guardrail* for your SDK, a Store-side pattern such as one matching `1.x`-style version tracks. Without a matching guardrail, **sdkcraft create-track** is rejected. Guardrails are not self-service. To request one, open a [GitHub issue](https://github.com/canonical/workshop/issues) on the **Workshop** repository, naming the SDK and the track pattern you need (for example, version tracks like `1.x` or `2.x`). The **Workshop** team triages the request and coordinates track creation with the SDK Store team. For the level of detail a request should carry, see how the wider ecosystem handles [snap track and guardrail requests](https://forum.snapcraft.io/t/create-new-track-and-guardrails-for-registry-snap/51209). ## Consume the published SDK Once a revision is released to a channel, any **Workshop** user can pull it by referencing the SDK in `workshop.yaml`: ```yaml name: dev base: ubuntu@24.04 sdks: - name: channel: latest/stable ``` The workshop’s `base` must match one of the SDK’s supported platforms. ## See also Explanation: - [SDK concepts](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-concepts) - [Testing and trying SDKs](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-test-try-sdk) How-to guides: - [How to build an SDK](https://ubuntu.com/workshop/docs//how-to/develop-sdks/build-an-sdk.md#how-build-sdk) Reference: - [sdkcraft create-track](https://ubuntu.com/workshop/docs//reference/cli/sdkcraft.md#ref-sdkcraft-create-track) - [sdkcraft login](https://ubuntu.com/workshop/docs//reference/cli/sdkcraft.md#ref-sdkcraft-login) - [sdkcraft pack](https://ubuntu.com/workshop/docs//reference/cli/sdkcraft.md#ref-sdkcraft-pack) - [sdkcraft register](https://ubuntu.com/workshop/docs//reference/cli/sdkcraft.md#ref-sdkcraft-register) - [sdkcraft release](https://ubuntu.com/workshop/docs//reference/cli/sdkcraft.md#ref-sdkcraft-release) - [sdkcraft test](https://ubuntu.com/workshop/docs//reference/cli/sdkcraft.md#ref-sdkcraft-test) - [sdkcraft try](https://ubuntu.com/workshop/docs//reference/cli/sdkcraft.md#ref-sdkcraft-try) - [sdkcraft upload](https://ubuntu.com/workshop/docs//reference/cli/sdkcraft.md#ref-sdkcraft-upload) - [Workshop definition](https://ubuntu.com/workshop/docs//reference/definition-files/workshop-definition.md#ref-workshop-definition) Tutorial: - [Craft SDKs with SDKcraft](https://ubuntu.com/workshop/docs//tutorial/part-4-craft-sdks.md#tut-craft-sdks) # purge.md # How to purge malfunctioning workshops Workshops can sometimes become unresponsive, encounter errors during start or stop operations, or become orphaned if their project directory is removed prematurely. A thorough purge involves removing the workshop’s containers, metadata, and files in a deliberate sequence. ## Standard removal procedure The primary command for removing a workshop is: ```console $ workshop remove ``` This command is designed to: - Stop the workshop if it is running. - Delete the underlying LXD container. - Remove associated workshop data and cache directories. - Clean up related LXD profiles and remove device mounts. Always attempt this command first. If it completes successfully, your workshop should be purged. You can verify the outcome by running **workshop list**. ## If standard procedure fails You may need manual intervention if: - **workshop remove** fails with an error. - The workshop is still listed by **workshop list** or **workshop list --global** after a remove attempt. - The workshop’s project directory had been deleted before you attempted to remove the workshop, potentially orphaning LXD resources. - The workshop is in an unrecoverable error state. - The workshop’s container is still running or in an error state, preventing standard. If the standard procedure is ineffective for any of the above reasons, you will need to manually clean up the workshop’s resources. For this, you interact directly with LXD and the workshop’s snap data. ### Find LXD project Workshop creates LXD projects named `workshop.`, where `` is your system username. You’ll also need your username for some paths. ### Clean up LXD resources Refer to the [Explore LXD containers](https://ubuntu.com/workshop/docs//how-to/fix-workshops/fix-installation.md#how-troubleshoot-lxc) section in the troubleshooting guide for initial steps on listing and deleting orphaned LXD containers, e.g.: ```console $ sudo lxc list --all-projects | grep workshop. $ sudo lxc delete --project workshop. --force ``` To ensure there are no backup copies of the workshop remaining, check the `workshop-snapshots.` project as well: ```console $ sudo lxc list --all-projects | grep workshop-snapshots. $ sudo lxc delete --project workshop-snapshots. --force ``` In addition to containers, you may need to clean up associated LXD profiles. #### LXD profiles Workshops create an LXD profile for each SDK they use. These profiles are named `-`. If a workshop container wasn’t cleanly removed, its profiles might remain. - List profiles for your workshop user project: ```console $ sudo lxc profile list --project workshop. ``` - Inspect a specific profile: ```console $ sudo lxc profile show --project workshop. ``` - Delete an orphaned profile. To ensure it’s not in use by other valid workshops, list all containers in the project firstly: ```console $ sudo lxc list --project workshop. ``` Then, for each container that should remain, check its configuration to see which profiles it uses: ```console $ sudo lxc config show --project workshop. ``` Look for the `profiles` key in the output. If the `` you intend to delete is not listed for any relevant containers, it should be safe to remove: ```console $ sudo lxc profile delete --project workshop. ``` - To delete an orphaned profile, check the `USED BY` column in the output of the **lxc profile list** command. If the count is zero, the profile is not used by any containers and can be safely removed. ## Aggressive cleanup If previous steps haven’t resolved the issue, or if **workshop list** still shows remnants, the most aggressive cleanup method is to completely purge the **Workshop** snap. This executes the snap’s `remove` hook, which is designed to clean up all associated data and resources. To purge the snap and all its data, run the following command: ```console $ sudo snap remove workshop --purge ``` This will remove all workshop configurations, containers, LXD profiles, and storage pools managed by **Workshop**. After the command completes, you can reinstall the snap. #### WARNING This is a highly destructive operation that removes all workshops for all users on the system. It should only be used as a last resort. You will need to reinstall **Workshop** to use it again. ## Final checks After performing manual cleanup steps: - Run **workshop list --global** to check if the malfunctioning workshop is no longer listed. - Run **sudo lxc list --all-projects** to ensure no unexpected LXD resources remain. If issues persist, consider seeking community support, or reporting a bug with detailed logs and steps taken: [Project and community](https://ubuntu.com/workshop/docs//index.md#project-community). ## See also How-to guides: - [How to debug issues in workshops](https://ubuntu.com/workshop/docs//how-to/fix-workshops/debug-issues.md#how-debug-issues-workshops) - [How to troubleshoot Workshop](https://ubuntu.com/workshop/docs//how-to/fix-workshops/fix-installation.md#how-troubleshoot) Reference: - [workshop list](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-list) - [workshop remove](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-remove) # reference.md # Reference These reference guides provide technical background that may be required to use **Workshop** and **SDKcraft**. ## Command-line interfaces **Workshop** and **SDKcraft** ship with a small set of command-line tools: * [CLI](https://ubuntu.com/workshop/docs//reference/cli/index.md) * [sdk (CLI)](https://ubuntu.com/workshop/docs//reference/cli/sdk.md) * [sdkcraft (CLI)](https://ubuntu.com/workshop/docs//reference/cli/sdkcraft.md) * [workshop (CLI)](https://ubuntu.com/workshop/docs//reference/cli/workshop.md) * [workshopctl (CLI)](https://ubuntu.com/workshop/docs//reference/cli/workshopctl.md) ## Definition file formats Workshops and SDKs are defined in YAML and share a number of basic elements such as plugs, base images and so on. However, both definition types have different purposes and structure: * [Definition files](https://ubuntu.com/workshop/docs//reference/definition-files/index.md) * [Workshop definition](https://ubuntu.com/workshop/docs//reference/definition-files/workshop-definition.md) * [SDK definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdk-definition.md) * [SDKcraft project definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdkcraft-definition.md) ## Structure and behavior Workshops are largely made of SDKs; understanding how a workshop runs and the status it carries starts with how SDKs are structured and operated at run-time: * [SDK internals](https://ubuntu.com/workshop/docs//reference/sdks.md) * [Workshop internals](https://ubuntu.com/workshop/docs//reference/workshops.md) * [Workshop status diagrams](https://ubuntu.com/workshop/docs//reference/workshop-status.md) ## AI agents **Workshop** exposes documentation in LLM-readable form and ships two agentic skills that wrap its CLIs for coding agents: * [Workshop and AI agents](https://ubuntu.com/workshop/docs//reference/ai-agents.md) ## Reference implementations These real-life examples on GitHub, maintained by the **Workshop** team, are meant to showcase different SDK patterns and workshop implementations. Study them to better understand SDK design and workshop creation: - [https://github.com/canonical/reference-sdks](https://github.com/canonical/reference-sdks) - [https://github.com/canonical/reference-workshops](https://github.com/canonical/reference-workshops) # release-notes.md # Release notes and upgrade instructions Each version brings new features, bug fixes, and occasionally backwards-incompatible changes. Where necessary, these release notes also include specific upgrade instructions for each version. For additional guidance, see the [general instructions on preparing for and performing an upgrade](#release-upgrade). ## Releases A complete **Workshop** installation comprises two snaps: - **workshop** is designed for common users. - **sdkcraft** is intended for SDK publishers. Both are available for `amd64` and `arm64`. Starting with 0.9.1, **Workshop** and **SDKcraft** share the same version number. ### Latest version - [Workshop and SDKcraft 0.9.1](https://ubuntu.com/workshop/docs//release-notes/v0.9.1.md) ### **Workshop** #### NOTE These versions are no longer supported. - [Workshop v0.9.0](https://ubuntu.com/workshop/docs//release-notes/v0.9.0.md) - [Workshop v0.1.30](https://github.com/canonical/workshop/releases/tag/v0.1.30) - [Workshop v0.1.29](https://github.com/canonical/workshop/releases/tag/v0.1.29) ### **SDKcraft** #### NOTE These versions are no longer supported. - [SDKcraft 0.1.14](https://github.com/canonical/sdkcraft/releases/tag/0.1.14) ## Release policy and schedule Our release cadence is biweekly, aligned with our development methodology. Releases follow the [semantic versioning](https://semver.org/) scheme. ### Long-term support We only provide support for the latest versions of **Workshop** and **SDKcraft**. If you encounter issues with an older version, we recommend upgrading to the latest release first; see the next section for guidance. ## Upgrade instructions Refresh the snaps using the [--classic](https://snapcraft.io/docs/install-modes/) option: ```console $ sudo snap refresh --classic workshop $ sudo snap refresh --classic sdkcraft ``` For prerequisites and other details, see the [Installation](https://github.com/canonical/workshop?tab=readme-ov-file#installation) section on GitHub, or follow the [Get started with workshops](https://ubuntu.com/workshop/docs//tutorial/part-1-get-started.md#tut-get-started) tutorial section. # resolve-plug-conflicts.md # How to fix plug conflicts with binding The example below binds plugs for the `mount` interface, but the same process works for any other interface that supports bindings. Suppose you have two SDKs that each declare a plug of the same interface, and the plugs are in conflict. Here, we use fictional `torchaudio:hub` and `torchvision:hub` that both point to the `~/.cache/torch/hub` directory on the host, where the SDKs store their models. 1. Create or open your workshop definition and list both SDKs: ```yaml name: digits base: ubuntu@22.04 sdks: - name: torchaudio - name: torchvision ``` Launching this workshop would cause a conflict because both SDKs want to mount the same directory in the workshop, which is not allowed. 1. To address this issue, bind the `torchvision:hub` plug to the `torchaudio:hub` plug by adding a `bind` attribute in the workshop definition: ```yaml name: digits base: ubuntu@22.04 sdks: - name: torchaudio - name: torchvision plugs: hub: bind: torchaudio:hub ``` 2. Launch the workshop. **Workshop** now recognizes that `torchvision:hub` is bound to `torchaudio:hub` and therefore mounts a single directory for both plugs. ```console $ workshop launch digits ``` 3. Verify the binding with **workshop connections**: ```console $ workshop connections digits INTERFACE PLUG SLOT NOTES mount digits/torchaudio:hub digits/system:mount bind.1 mount digits/torchvision:hub digits/system:mount bind.1 ``` Both plugs share the same `bind.1` note, which implies they reference the same mount. 4. Any operation on one side automatically applies to the other. For example, after remounting `torchaudio:hub`, the information for `torchvision:hub` is updated as well: ```console $ mkdir -p .cache/hub $ workshop remount digits/torchaudio:hub .cache/hub $ workshop info digits ... mounts: hub: host-source: /home/user/digits/.cache/hub workshop-target: /home/workshop/.cache/torch/hub ``` ## See also Explanation: - [Plug bindings](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-plug-bindings) Reference: - [workshop connections](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connections) - [Workshop definition](https://ubuntu.com/workshop/docs//reference/definition-files/workshop-definition.md#ref-workshop-definition) # run-github-actions-locally.md # How to run GitHub Actions locally Running GitHub Actions locally provides a powerful way to develop, test, and debug CI/CD workflows. This approach offers faster feedback loops and greater control over the execution environment while maintaining compatibility with your existing GitHub Actions workflows. The `github-runner` SDK lets a workshop act as a [just-in-time runner](https://docs.github.com/en/actions/reference/security/secure-use#using-just-in-time-runners) for [GitHub workflow jobs](https://docs.github.com/en/actions/concepts/workflows-and-actions/workflows). Running jobs locally makes a few things easier: - Inspecting logs and other files after a failed run - Interactive debugging, profiling, and tracing - Testing with new, unusual, or expensive hardware - Shorter feedback loops while ensuring consistency with remote runs ## Prerequisites Before getting started, ensure you have: - **Workshop** installed and properly configured - A GitHub account with admin permissions on the target repository, or “self-hosted runners” permission for organization-level runners ## Set up the workshop To run GitHub Actions locally, create or update your workshop definition to include the `github-runner` SDK: ```yaml name: ci base: ubuntu@24.04 sdks: - name: github-runner ``` This installs the official [Runner](https://github.com/actions/runner) client and an unofficial helper script named `github-runner`. Don’t forget to launch or refresh the workshop. #### NOTE GitHub-hosted runners have a lot of preinstalled software, most of which isn’t included in workshops by default. If a workflow-based run fails because of missing software, we recommend installing it as part of the workflow. This makes local and remote runs more consistent. Some actions (e.g., [setup-python](https://github.com/actions/setup-python)) provide additional features like caching. Some tools (notably Docker) aren’t as easy to install during a job, but are available as SDKs. Others (such as **yq**) are useful for development in addition to CI. These can be sketched into an SDK alongside `github-runner`; refer to the [See also]() section for details. ## Configure authorization An important step is to authorize the `github-runner` SDK to access your GitHub repositories or organization. ### Choose a repository or organization First, choose a repository or organization for the runner. Admin-level permissions are required to add a runner to a repository. Runners have access to [secrets](https://docs.github.com/en/actions/concepts/security/secrets), so these permissions should be carefully guarded. Users without admin rights can fork the repository and test their workflows in the fork. Another option is to add a runner to an organization, which doesn’t require admin rights on the organization, but does grant access to organization secrets. Proceed at your own risk. ### Share permissions The `github-runner` script needs the above permissions to add the runner on your behalf. When it runs for the first time, it will request authorization using a one-time code. To limit the SDK’s access to the necessary repositories, the request is mediated by a [GitHub App](https://docs.github.com/en/apps/using-github-apps/about-using-github-apps) provided courtesy of the Workshop team. By default, the SDK can’t access any repository or organization on your behalf. To grant access, navigate to the [GitHub App for github-runner](https://github.com/apps/test-app-jonathan-conder-1) and install it. Once installed, the repositories it has access to can be configured at any time. If the workshop or host machine is compromised, the App should be uninstalled to limit the damage. - For individuals, the App should be installed on a personal account and granted access to the required repositories. - For organizations, the App must be installed on the organization. After adding a runner to the organization, workflows can use it even if the App is denied access to the relevant repository. Alternatively, the runner can be added to individual repositories within the organization. The App should be granted access to those repositories. Canonical only uses the App as a fine-grained authorization mechanism. The SDK doesn’t share information with Canonical or any third party (apart from GitHub). That said, if you prefer to use a different authentication mechanism, export the `GITHUB_TOKEN` environment variable inside the workshop. The `github-runner` script will use that if available. ## Run a workflow locally Now everything is set up to run a workflow locally. ### Start the runner Start the Runner client inside the workshop: ```console $ workshop exec ci github-runner --label=workshop [/] ``` Replace `/` with the full repository name (e.g., `canonical/workshop`). If omitted, the script tries to detect this information from the local repository. For organization-level runners, make sure to provide the organization name (e.g., `canonical`). The `--label` option adds a label to the runner, to distinguish it from GitHub-hosted runners and other self-hosted runners (if any). Use `--help` to see the full list of options. When the script runs for the first time, it will request authorization using a one-time code. Access can be revoked at any time via the [GitHub App](https://github.com/apps/test-app-jonathan-conder-1). After a few seconds, the runner should be ready for incoming jobs. The next step is to configure jobs to use the runner. ### Runner options The `github-runner` command supports several options: ```console $ workshop exec ci github-runner --help ``` Key options include: | Option | Description | |--------------|--------------------------------------------------------| | `--name` | Specify a unique runner name | | `--prefix` | Add a prefix to the runner name (defaults to hostname) | | `--label` | Add custom labels to the runner | | `--once` | Exit after running a single job | | `--group-id` | Add runner to a specific runner group | ### Configure your workflow Add the `workshop` label to the `runs-on` option in the workflow file. Consider making this configurable, if only to avoid repeatedly editing the workflow. For example: ```yaml on: pull_request: push: branches: [main] workflow_dispatch: inputs: runner: description: Where to run the job type: choice required: true options: [ubuntu-latest, workshop] default: ubuntu-latest jobs: test: runs-on: ["${{ inputs.runner || 'ubuntu-latest' }}"] steps: - uses: actions/checkout@v6 - run: make test ``` ### Run the workflow The specific steps depend on the workflow. For the above example: commit the updated workflow to the `main` branch, find it in the Actions tab of the repository, and select Run workflow. Pick whichever branch you like, as long as the runner is set to `workshop`. The Runner client should print a few logs when a job starts and finishes. Full logs can still be viewed on GitHub. ## Tips Take care when logging. Some actions could leak sensitive information about the runner, such as its IP address. --- The Runner client runs one job at a time. To run several jobs in parallel, use multiple workshops. For example: ```console $ mkdir -p .workshop $ mv workshop.yaml .workshop/ci.yaml $ sed 's/name: ci/name: ci2/' <.workshop/ci.yaml >.workshop/ci2.yaml $ workshop launch ci2 $ workshop exec ci2 github-runner --label=workshop ``` --- The Runner client doesn’t clean up after itself. This can be helpful for debugging but may cause issues for some workflows. To avoid these issues, refresh the workshop after each job. For example: ```console $ while workshop exec ci github-runner --label=workshop --once; do workshop refresh ci done ``` --- In rare cases (like a power outage at the wrong time), runners can remain attached to the repository indefinitely. These can be removed manually in the repository or organization settings. --- For quick iteration, the runner can be made conditional on the branch name: ```yaml on: push: branches: - main - workshop-runner/** jobs: test: runs-on: ["${{ startsWith(github.ref_name, 'workshop-runner/') && 'workshop' || 'ubuntu-latest' }}"] steps: - uses: actions/checkout@v6 - run: make test ``` ## Security considerations When running actions locally: - Be cautious with secrets and sensitive data - Mind that actions may leak information about your local environment - Consider using separate workshops for different projects - Regularly review and rotate access tokens - Monitor actions for unexpected behavior ## See also How-to guides: - [How to use workshops with Git](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/use-git.md#how-git-workshops) Tutorial: - [Customize with sketch SDKs](https://ubuntu.com/workshop/docs//tutorial/part-3-sketch-sdks.md#tut-sketch-sdks) # run-jetbrains-gateway.md # How to use JetBrains Gateway with Workshop [JetBrains Gateway](https://www.jetbrains.com/remote-development/gateway/) allows you to connect to remote development environments while using your favorite JetBrains IDEs (IntelliJ IDEA, PyCharm, and so on). A workshop can serve as the remote development target for JetBrains Gateway, letting you use your favorite JetBrains IDEs against **Workshop**. ## Prerequisites - Download and install [JetBrains Gateway](https://www.jetbrains.com/remote-development/gateway/) on your host system. - Create a workshop or choose an existing one. - Generate an SSH key pair if you don’t have one already: ```console $ ssh-keygen -t rsa -b 4096 -C "" ``` ## Configure your workshop Configure your workshop to accept SSH connections by adding a plug, a slot, and an action to upload your public SSH key. 1. First, add a tunnel interface plug for the system SDK in the workshop definition: ```yaml sdks: - name: system plugs: gateway: interface: tunnel endpoint: 2200 ``` This exposes port `2200` on the host that you will use in JetBrains Gateway. 2. Next, add a corresponding slot; you can graft it onto an existing SDK or add it with sketching: 1. Add an action to upload your public SSH key to the workshop: ```yaml actions: upload-public-key: | PUBLIC_KEY="$1" if [ -z "${PUBLIC_KEY}" ]; then echo 'cannot upload public key: pass the public key as the argument' 1>&2 exit 1 fi echo "${PUBLIC_KEY}" >> $HOME/.ssh/authorized_keys ``` This appends your public SSH key to the list of authorized keys for the `workshop` user. 2. Refresh the workshop to apply the changes if you haven’t done so already: ```console $ workshop refresh ``` 3. Use the action to upload your public SSH key to the workshop, for example: ```console $ workshop run dev upload-public-key "$(cat ~/.ssh/id_rsa.pub)" ``` The workshop is now ready to accept SSH connections. ## Connect with Gateway 1. Open JetBrains Gateway. 2. Create a new SSH connection using these values: - Username: `workshop` - Host: `localhost` - Port: `2200` - Specify private key: Your *private* SSH key counterpart to the public key you uploaded earlier. 3. Click Check connection and continue. 4. At the next screen, select your preferred JetBrains IDE version, e.g. PyCharm. 5. Under Installation options, choose Customize installation path and set it to a path under `/project/`, e.g. `/project/pycharm/`; this ensures the IDE has enough disk space. 6. For Project directory, choose `/project/`. 7. Click Start IDE and connect, then wait for the IDE to install and launch; this may take a few minutes. After the IDE starts, log in and proceed as usual. Your JetBrains IDE will now run remotely in the workshop while providing a native desktop experience. ## See also Explanation: - [Sketch SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sketch-sdk) - [System SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-system-sdk) - [Tunnel interface plug](https://ubuntu.com/workshop/docs//explanation/interfaces/tunnel-interface.md#exp-tunnel-plug) - [Tunnel interface slot](https://ubuntu.com/workshop/docs//explanation/interfaces/tunnel-interface.md#exp-tunnel-slot) - [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) Reference: - [Tunnel interface](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-tunnel-interface) - [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh) - [workshop run](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-run) - [workshop sketch-sdk](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-sketch-sdk) # run-jupyterlab-in-browser.md # How to run JupyterLab in your browser A JupyterLab instance can run inside a workshop and be accessed from your browser via the `jupyter` SDK. To do that, add the `jupyter` SDK and configure a tunnel interface plug for the `system` SDK: ```yaml name: dev base: ubuntu@24.04 sdks: - name: system plugs: jupyter: interface: tunnel endpoint: 127.0.0.1:8989 - name: jupyter ``` Launch the workshop. After that, JupyterLab will be available in your browser at the plug address, e.g., [http://localhost:8989](http://localhost:8989). It starts as a user service with `/project/` as the default working directory to serve from. You can immediately start using it with any other SDKs you have installed. ## See also Explanation: - [System SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-system-sdk) - [Tunnel interface plug](https://ubuntu.com/workshop/docs//explanation/interfaces/tunnel-interface.md#exp-tunnel-plug) - [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) Reference: - [Tunnel interface](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-tunnel-interface) - [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) # run-workshops-in-github-actions.md # How to run workshops in GitHub Actions The [launch-workshop](https://github.com/canonical/launch-workshop) action installs **Workshop** on a GitHub-hosted runner and launches an ephemeral workshop for the duration of a job. Use it to run your project’s tests, builds, or other tasks inside the same workshop you use locally, without standing up a self-hosted runner. If you’d rather run jobs on your own hardware, use a workshop as a self-hosted runner instead; see [How to run GitHub Actions locally](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/run-github-actions-locally.md#how-run-github-actions-locally). ## Prerequisites Before getting started, ensure you have: - A GitHub repository with Actions enabled - A workshop definition committed to the repository, at `workshop.yaml` or `.workshop/.yaml` - A [personal access token](https://github.com/settings/tokens?type=beta) with `Contents: read` and `Metadata: read` permissions on `canonical/workshop` ## Configure the workshop token The action installs **Workshop** from `canonical/workshop`, which is an internal repository. The token granting read access to that repository must be stored as an Actions secret in your project repository. In your project repository on GitHub, navigate to Settings > Secrets and variables > Actions, select New repository secret, and add the token under the name `WORKSHOP_TOKEN`. The action reads this secret via the `token` input. ## Add the action to a workflow The smallest useful workflow checks out the repository, launches the default workshop, and runs a command inside it: ```yaml on: pull_request: push: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: canonical/launch-workshop@v0 with: token: ${{ secrets.WORKSHOP_TOKEN }} - run: workshop exec -- pytest ``` If the repository contains a single `workshop.yaml` at the project root, the action launches it automatically. For repositories with several workshops under `.workshop/`, set the `workshop` input to the one you need. For production use, pin the action to a specific commit SHA. The `@v0` shorthand shown above tracks the latest `v0.*` release and can be moved between versions; `@main` is even less stable. ## Test across multiple workshops To test a project against several workshops in parallel, parameterize the `workshop` input with a matrix: ```yaml jobs: test: runs-on: ubuntu-latest strategy: matrix: workshop: [dev-jammy, dev-noble] steps: - uses: actions/checkout@v6 - uses: canonical/launch-workshop@v0 with: token: ${{ secrets.WORKSHOP_TOKEN }} workshop: ${{ matrix.workshop }} - run: workshop run "$WS" unit-tests env: WS: ${{ matrix.workshop }} ``` This pattern fits well for testing the same project against different Ubuntu releases, different SDK channels, or any other axis you encode in the workshop name. ## Cache SDK data across runs Some SDKs expose mount plugs that can be persisted between workflow runs, such as a package cache or a build cache. List the available plugs in your local workshop with **workshop connections**: ```console $ workshop connections --all INTERFACE PLUG SLOT NOTES mount python:pip-cache system:mount - ``` Pass the matching `:` lines to the `cache` input, one per line: ```yaml steps: - uses: canonical/launch-workshop@v0 with: token: ${{ secrets.WORKSHOP_TOKEN }} cache: | cargo:git cargo:registry go:mod-cache python:pip-cache ``` Each listed plug is mounted from a GitHub-managed cache, keyed by the SDK and plug name. Not every SDK defines cacheable plugs; check the SDK’s documentation when in doubt. ## Inputs The action exposes the following inputs: | Input | Description | |------------|----------------------------------------------------------------------------------------| | `token` | Access token for `canonical/workshop`. Required. | | `version` | **Workshop** version or range of versions. Defaults to `latest`. | | `project` | Directory containing a workshop to launch.
Defaults to the repository root. | | `workshop` | Name of the workshop to launch.
Required if the project defines several workshops. | | `cache` | Mount plugs to cache across runs,
one `:` entry per line. | ## Security considerations When integrating the action into your workflows: - Store the token as an Actions secret; never commit it to the repository or paste it into logs. - Prefer a fine-grained personal access token scoped to `canonical/workshop` with only `Contents: read` and `Metadata: read` permissions. - Pin the action to a commit SHA so a compromised tag cannot push unreviewed code into your workflows. - Rotate `WORKSHOP_TOKEN` immediately if it ever appears in logs, chat history, or any other shared transcript. ## See also How-to guides: - [How to use workshops with Git](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/use-git.md#how-git-workshops) - [How to run GitHub Actions locally](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/run-github-actions-locally.md#how-run-github-actions-locally) # runtime-behavior.md # Runtime behavior **Workshop**’s system components work together to transform a workshop definition into a running container, following a set of dynamic processes and workflows that drive workshop operations. #### NOTE For a general description of these components, see [System components](https://ubuntu.com/workshop/docs//explanation/architecture/components.md#exp-arch-system-components). ## Workshop launch process The workshop launch process coordinates the [workshopd daemon](https://ubuntu.com/workshop/docs//explanation/architecture/components.md#exp-arch-daemon), [LXD backend](https://ubuntu.com/workshop/docs//explanation/architecture/components.md#exp-arch-lxd-backend), [state management](https://ubuntu.com/workshop/docs//explanation/architecture/components.md#exp-arch-state-database), [interface policy](https://ubuntu.com/workshop/docs//explanation/architecture/components.md#exp-arch-interface-system), and [ZFS storage](https://ubuntu.com/workshop/docs//explanation/architecture/components.md#exp-arch-zfs-storage) subsystems, all detailed in the previous section. The launch sequence begins with workshop YAML validation and normalization. The LXD container is then created from a base image. The [system SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-system-sdk) is installed first to provide the core integration layer with host system resources. Regular SDKs are installed sequentially. SDK-specific [setup hooks](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks) are executed during this phase. Workshop creates a lightweight ZFS clone after each SDK installation to enable efficient updates and rollbacks. Interface validation performs compatibility checking and policy enforcement. Connection establishment handles device attachment and resource binding. The container is then started and health checks are performed to complete the launch. - Launch operations create new workshops from scratch by creating new LXD containers and projects as needed, downloading and caching base images as required, installing all SDKs with complete hook execution, and establishing all interface connections. - Refresh operations update existing workshops by preserving LXD container identity and networking, restoring to base snapshot before applying changes, skipping unchanged SDKs using snapshot comparison, running save-state and restore-state hooks for SDK data persistence, and updating only modified interface connections. Workshop status transitions follow a predictable lifecycle: - *Off* → *Pending*: Workshop creation is initiated by user request - *Pending* → *Waiting*: Launch encounters errors requiring manual intervention - *Pending* → *Stopped*: Launch succeeds but the container remains stopped - *Stopped* → *Ready*: Container starts successfully and becomes available for use These changes are tracked in the state management system and can be monitored through the API. #### NOTE For a complete reference guide on status transitions, see [Workshop status diagrams](https://ubuntu.com/workshop/docs//reference/workshop-status.md#ref-workshop-status). ## Container layout Container runtime setup builds on the [LXD backend](https://ubuntu.com/workshop/docs//explanation/architecture/components.md#exp-arch-lxd-backend) foundation with runtime-specific configuration applied during workshop launch. Key directories inside the container include: - The root file system as a ZFS dataset. - The `/project/` mount to provide transparent access to project files on the host. - The `/var/lib/workshop/` directory where workshop state volume and SDK volumes are mounted. The [interface system](https://ubuntu.com/workshop/docs//explanation/architecture/components.md#exp-arch-interface-system) provisions different resources within containers: - Mount interfaces appear as regular directories. - Proxy devices handle port forwarding and services such as the SSH agent. - GPU, audio, and camera devices are passed through to the workshop with proper permissions and access controls. ## Diagrams Workshop launch flow: # sdk-cli.md # sdk (CLI) **Workshop** includes an **sdk** command-line utility; it has a set of commands that make it easy to find and learn more about SDKs. There is one category of commands: | Actions | Commands | What they do | |-----------|--------------------------------------|--------------------------------------------------------------------------------------------------------| | Discover | **find**,
**info**,
**list** | Search the SDK Store,
enumerate the SDKs available on your machine,
and inspect their details. | #### NOTE The utility talks to the **Workshop** daemon, **workshopd**, via a REST API, so alternatives are possible and, in fact, encouraged. ## See also Reference: - [Command-line interfaces](https://ubuntu.com/workshop/docs//reference/index.md#ref-cli) Tutorial: - [Get started with workshops](https://ubuntu.com/workshop/docs//tutorial/part-1-get-started.md#tut-get-started) # sdk-definition.md # SDK definition The `sdk.yaml` file is the *runtime* SDK definition: **Workshop** reads it when it installs an SDK in a workshop. For Store SDKs and SDKs from **sdkcraft try**, this file is produced by **SDKcraft** from `sdkcraft.yaml` (see [SDKcraft project definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdkcraft-definition.md#ref-sdkcraft-definition)). For sketch SDKs and in-project SDKs, you author `sdk.yaml` directly: sketch SDKs through **workshop sketch-sdk**, in-project SDKs by hand under `.workshop/`. ## Filename and location - Store SDKs and SDKs from **sdkcraft try** ship `sdk.yaml` inside their packed contents at `meta/sdk.yaml`. - In-project SDKs use `.workshop//sdk.yaml` or `.workshop//meta/sdk.yaml`, relative to the project directory. Their hook scripts live next to the definition, under `.workshop//hooks/`. - Sketch SDK definitions live in the per-workshop data directory: `~/.local/share/workshop/id///sdk/sketch/current/sdk.yaml`. In-project and sketch SDKs do not support **SDKcraft** build-time features such as `build-base`, `platforms`, or `parts`. These belong to `sdkcraft.yaml`. ## Top-level fields | Key | Value | Description | |-------------------------------------------------|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `name` (required) | string | SDK identifier. Must contain at least one lowercase letter
and may consist of lowercase letters, digits, and hyphens between them.
Up to 40 characters.
Cannot be `agent`, `system`, `sketch`,
or start with `try-` or `project-`; those names are reserved. | | `architecture` (required for built SDKs) | string | CPU architecture the SDK is built for,
following [Debian’s naming scheme](https://www.debian.org/ports/)
(for example, `amd64`, `arm64`).
Use `all` for SDKs that ship no compiled binaries. | | `version` (required for built SDKs) | string | SDK version. Semantic versioning is recommended.

#### NOTE
Quote version strings in YAML when they look numeric
(for example, `version: "1.0"`) to avoid type coercion. | | `summary` (required for built SDKs) | string | One-line summary, up to 78 characters. | | `description` (required for built SDKs) | string | Longer free-form description, up to about a hundred words. | | `sdkcraft-started-at` (required for built SDKs) | string | UTC timestamp marking when **SDKcraft** started the build.
Set automatically; do not edit by hand. | | `base` | string | Base operating system image the SDK targets.
One of `ubuntu@20.04`, `ubuntu@22.04`, `ubuntu@24.04`,
or `ubuntu@26.04`.
Omit for SDKs that work on any supported base. | | `title` | string | Human-readable title. | | `license` | string | License name, as it would appear in package metadata.

#### NOTE
Match the license to the actual components the SDK installs. | | `contact` | string, array, or URL | Contact information for the SDK publisher. | | `issues` | string, array, or URL | Where users should report problems with the SDK. | | `source-code` | URL | Where the SDK’s source code is hosted. | | `website` | URL | The web page for the SDK. | | `plugs` | object | Plugs the SDK requests from the workshop environment.
Each key is the plug name; each value is an inline plug definition.
See [Interfaces](#ref-sdk-definition-interfaces). | | `slots` | object | Slots the SDK provides.
Each key is the slot name;
each value is an inline slot definition.
Only the `mount` and `tunnel` interfaces
support slots on regular SDKs.
See [Interfaces](#ref-sdk-definition-interfaces). | #### NOTE “Required for built SDKs” means **SDKcraft** writes the field when it builds an SDK package; for an in-project SDK, you can author `sdk.yaml` with only `name`, plus whichever optional fields you need. In particular, `architecture` for in-project SDKs is assumed to match the host (or `all`). ## Interfaces A plug or slot value is an inline definition: a mapping that specifies the `interface` and any interface-specific attributes. ### Camera interface The camera interface exposes a host camera device. - Plug attributes: none. - Plug name: must be `camera`. - Plug owner: any regular SDK; not the system SDK. - Slot: the system SDK provides a single `system:camera` slot. Other SDKs cannot declare camera slots. ### Custom device interface The custom device interface exposes host devices that belong to a Linux kernel subsystem. A custom device plug is described by this attribute: | Key | Value | Description | |------------------------|---------|-----------------------------------------------------------------------------------------------------| | `subsystem` (required) | string | The Linux kernel subsystem of the host devices to expose,
for example `input`, `tty`, or `usb`. | Plug owner: any regular SDK; not the system SDK. Slot: the system SDK provides a single `system:custom-device` slot. Other SDKs cannot declare custom device slots. ### Desktop interface The desktop interface exposes the host display server. - Plug attributes: none. - Plug name: must be `desktop`. - Plug owner: any regular SDK; not the system SDK. - Slot: the system SDK provides a single `system:desktop` slot. Other SDKs cannot declare desktop slots. ### GPU interface The GPU interface exposes host GPU devices. - Plug attributes: none. - Plug name: must be `gpu`. - Plug owner: any regular SDK; not the system SDK. - Slot: the system SDK provides a single `system:gpu` slot. Other SDKs cannot declare GPU slots. ### Mount interface The mount interface exposes a directory between a slot owner and a plug owner. A mount plug is described by these attributes: | Key | Value | Description | |------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `workshop-target` (required) | string | Path inside the workshop used as the plug’s target directory.
Must be an absolute path;
`$SDK` expands to the SDK’s installation path in the workshop. | | `mode` | integer | File permissions, in octal, applied when creating `workshop-target`
and any missing parent directories.
Defaults to `0o775` for regular users.
When `uid` is zero, defaults to `0o755`. | | `uid` | integer | User ID applied when creating `workshop-target`
and any missing parent directories.
Defaults to `1000` when `workshop-target` is under
`/home/workshop/`, `/project/`, or `/run/user/1000/`.
Defaults to `0` otherwise. | | `gid` | integer | Group ID applied when creating `workshop-target`
and any missing parent directories.
Defaults to `1000` or `0`
by the same path rule as `uid`,
even when `uid` is set explicitly. | | `read-only` | Boolean | Whether the target directory should be read-only.
Defaults to `false`. | Plug owner: any regular SDK; not the system SDK. The system SDK provides one mount slot, `system:mount`, with a dynamic `host-source` attribute that can be configured only at [remount](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-remount). It is the only mount slot whose source is on the host filesystem. A mount slot on a regular SDK is described by this attribute: | Key | Value | Description | |------------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| | `workshop-source` (required) | string | Path inside the workshop used as the slot’s source directory.
Must be an absolute path;
`$SDK` expands to the SDK’s installation path in the workshop. | ### SSH interface The SSH interface exposes the user’s SSH agent socket. - Plug attributes: none. - Plug name: must be `ssh-agent`. - Plug owner: any regular SDK; not the system SDK. - Slot: the system SDK provides a single `system:ssh-agent` slot. Other SDKs cannot declare SSH slots. ### Tunnel interface The tunnel interface forwards a network address or Unix domain socket. Both tunnel plugs and tunnel slots take a single attribute: | Key | Value | Description | |------------|---------|-----------------------------------------------------------------------------------------------------------------------------------| | `endpoint` | string | Network address or Unix domain socket that forms one end of the tunnel.
Defaults to `localhost/tcp` for both plugs and slots. | The `endpoint` value follows this grammar: | Field | Format | |----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Endpoint | `
/` for network endpoints;
may be shortened to `
` or `` alone.

`` or `@` for Unix domain sockets. | | Address | `:`; may be shortened to `` or ``. | | Protocol | Either `tcp` or `udp`. Defaults to `tcp`. | | Host | An IPv4 or IPv6 address.
When a port is supplied, IPv6 addresses must be enclosed in square brackets.

Supported aliases: `localhost`, `ip6-localhost`, and `ip6-loopback`.
Defaults to `localhost`. | | Port | A TCP or UDP port number (1-65535).
May be omitted, but only on one side of a connection; both sides then use the same port.

For security, tunnel plugs in the system SDK cannot use privileged ports (1-1023). | | Path | Absolute path to a Unix domain socket.

`$HOME` expands to the user’s home directory
and `$XDG_RUNTIME_DIR` expands to the user runtime directory
(typically `/run/user/1000`).

For security, tunnel plugs in the system SDK cannot listen on sockets outside these two directories. | | String | An abstract socket name. | Endpoints that start with `[` or `@` must be quoted in YAML: ```yaml endpoint: '[::1]:8080/tcp' endpoint: '@abstract.sock' ``` ## JSON Schema The following JSON Schema is exported from **SDKcraft**’s runtime metadata model and describes the structure above: #### NOTE The schema describes a *built* `sdk.yaml`, that is, the file **SDKcraft** writes when it packs an SDK. The `required` list reflects what a packed SDK must carry; for an in-project `sdk.yaml` you author by hand, only `name` is mandatory (see the note below the table). Numeric bounds use pydantic-style `ge`, `le`, and `lt` keywords. Generic JSON Schema validators will not enforce them; treat the bounds as documentation of the runtime’s accepted ranges, and rely on the field table above for the authoritative rules. ### SDK definition schema ```json { "$defs": { "CameraPlug": { "additionalProperties": false, "description": "SDKcraft project camera plug definition.", "properties": { "interface": { "const": "camera", "title": "Interface", "type": "string" } }, "required": [ "interface" ], "title": "CameraPlug", "type": "object" }, "CleanAbsPath": { "type": "string" }, "CustomDevicePlug": { "additionalProperties": false, "description": "SDKcraft project custom-device plug definition.", "properties": { "interface": { "const": "custom-device", "title": "Interface", "type": "string" }, "subsystem": { "description": "Device subsystem.", "examples": [ "accel", "usb" ], "minLength": 1, "title": "Subsystem", "type": "string" } }, "required": [ "interface", "subsystem" ], "title": "CustomDevicePlug", "type": "object" }, "DesktopPlug": { "additionalProperties": false, "description": "SDKcraft project desktop plug definition.", "properties": { "interface": { "const": "desktop", "title": "Interface", "type": "string" } }, "required": [ "interface" ], "title": "DesktopPlug", "type": "object" }, "Endpoint": { "type": "string" }, "FileMode": { "$ref": "#/$defs/Int", "ge": 0, "le": 511 }, "GPUPlug": { "additionalProperties": false, "description": "SDKcraft project GPU plug definition.", "properties": { "interface": { "const": "gpu", "title": "Interface", "type": "string" } }, "required": [ "interface" ], "title": "GPUPlug", "type": "object" }, "Int": { "type": "integer" }, "MountPlug": { "additionalProperties": false, "description": "SDKcraft project mount plug definition.", "properties": { "interface": { "const": "mount", "title": "Interface", "type": "string" }, "workshop-target": { "$ref": "#/$defs/CleanAbsPath" }, "uid": { "$ref": "#/$defs/UserGroupID" }, "gid": { "$ref": "#/$defs/UserGroupID" }, "mode": { "$ref": "#/$defs/FileMode" }, "read-only": { "default": false, "title": "Read-Only", "type": "boolean" } }, "required": [ "interface", "workshop-target" ], "title": "MountPlug", "type": "object" }, "MountSlot": { "additionalProperties": false, "description": "SDKcraft project mount slot definition.", "properties": { "interface": { "const": "mount", "title": "Interface", "type": "string" }, "workshop-source": { "$ref": "#/$defs/CleanAbsPath" } }, "required": [ "interface", "workshop-source" ], "title": "MountSlot", "type": "object" }, "Plug": { "discriminator": { "mapping": { "camera": "#/$defs/CameraPlug", "custom-device": "#/$defs/CustomDevicePlug", "desktop": "#/$defs/DesktopPlug", "gpu": "#/$defs/GPUPlug", "mount": "#/$defs/MountPlug", "ssh-agent": "#/$defs/SSHAgentPlug", "tunnel": "#/$defs/TunnelPlug" }, "propertyName": "interface" }, "oneOf": [ { "$ref": "#/$defs/CameraPlug" }, { "$ref": "#/$defs/CustomDevicePlug" }, { "$ref": "#/$defs/DesktopPlug" }, { "$ref": "#/$defs/GPUPlug" }, { "$ref": "#/$defs/MountPlug" }, { "$ref": "#/$defs/SSHAgentPlug" }, { "$ref": "#/$defs/TunnelPlug" } ] }, "PlugName": { "description": "The name of the plug. This is used when connecting and disconnecting.\n\nThe plug name must consist only of lower-case ASCII letters (``a-z``), numerals\n(``0-9``), and hyphens (``-``). It must start with a letter, not end with a\nhyphen, and not contain two consecutive hyphens.\n", "examples": [ "desktop", "gpu", "ssh-agent" ], "pattern": "^[a-z](-?[a-z0-9])*$", "title": "Plug Name", "type": "string" }, "Plugs": { "additionalProperties": { "$ref": "#/$defs/Plug" }, "propertyNames": { "$ref": "#/$defs/PlugName" }, "type": "object" }, "ProjectName": { "description": "The name of the project. This is used when uploading, publishing, or installing.\n\nThe project name must consist only of lower-case ASCII letters (``a``-``z``), numerals\n(``0``-``9``), and hyphens (``-``). It must contain at least one letter, not start or\nend with a hyphen, and not contain two consecutive hyphens. The maximum length is 40\ncharacters.\n", "examples": [ "ubuntu", "jupyterlab-desktop", "lxd", "digikam", "kafka", "mysql-router-k8s" ], "maxLength": 40, "minLength": 1, "pattern": "(?!^(system|try-.*|project-.*|sketch)$)^([a-z0-9][a-z0-9-]?)*[a-z]+([a-z0-9-]?[a-z0-9])*$", "title": "Project Name", "type": "string" }, "SSHAgentPlug": { "additionalProperties": false, "description": "SDKcraft project SSH agent plug definition.", "properties": { "interface": { "const": "ssh-agent", "title": "Interface", "type": "string" } }, "required": [ "interface" ], "title": "SSHAgentPlug", "type": "object" }, "Slot": { "discriminator": { "mapping": { "mount": "#/$defs/MountSlot", "tunnel": "#/$defs/TunnelSlot" }, "propertyName": "interface" }, "oneOf": [ { "$ref": "#/$defs/MountSlot" }, { "$ref": "#/$defs/TunnelSlot" } ] }, "SlotName": { "description": "The name of the slot. This is used when connecting and disconnecting.\n\nThe slot name must consist only of lower-case ASCII letters (``a-z``), numerals\n(``0-9``), and hyphens (``-``). It must start with a letter, not end with a\nhyphen, and not contain two consecutive hyphens.\n", "examples": [ "dashboard", "gdb", "toolchain" ], "pattern": "^[a-z](-?[a-z0-9])*$", "title": "Slot Name", "type": "string" }, "Slots": { "additionalProperties": { "$ref": "#/$defs/Slot" }, "propertyNames": { "$ref": "#/$defs/SlotName" }, "type": "object" }, "TunnelPlug": { "additionalProperties": false, "description": "SDKcraft project tunnel plug definition.", "properties": { "interface": { "const": "tunnel", "title": "Interface", "type": "string" }, "endpoint": { "$ref": "#/$defs/Endpoint", "default": "" } }, "required": [ "interface" ], "title": "TunnelPlug", "type": "object" }, "TunnelSlot": { "additionalProperties": false, "description": "SDKcraft project tunnel plug definition.", "properties": { "interface": { "const": "tunnel", "title": "Interface", "type": "string" }, "endpoint": { "$ref": "#/$defs/Endpoint", "default": "" } }, "required": [ "interface" ], "title": "TunnelSlot", "type": "object" }, "UserGroupID": { "$ref": "#/$defs/Int", "ge": 0, "lt": 4294967295 } }, "additionalProperties": true, "description": "Structure to hold output metadata.", "properties": { "name": { "$ref": "#/$defs/ProjectName" }, "title": { "anyOf": [ { "description": "A human-readable title.", "examples": [ "Ubuntu Linux", "Jupyter Lab Desktop", "LXD", "DigiKam", "Apache Kafka", "MySQL Router K8s charm" ], "maxLength": 40, "minLength": 2, "title": "Title", "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "version": { "description": "The version of the project, enclosed in quotation marks.", "examples": [ "\"0.1\"", "\"1.0.0\"", "\"v1.0.0\"", "\"24.04\"" ], "maxLength": 32, "title": "version string", "type": "string" }, "summary": { "description": "A short description of the project. Maximum length 78 characters.", "examples": [ "Linux for Human Beings", "The cross-platform desktop application for JupyterLab", "Container and VM manager", "Photo Management Program", "Charm for routing MySQL databases in Kubernetes", "An open-source event streaming platform for high-performance data pipelines" ], "maxLength": 78, "title": "Summary", "type": "string" }, "description": { "title": "Description", "type": "string" }, "base": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Base" }, "architecture": { "title": "Architecture", "type": "string" }, "contact": { "anyOf": [ { "type": "string" }, { "items": { "type": "string" }, "type": "array", "uniqueItems": true }, { "type": "null" } ], "default": null, "title": "Contact" }, "issues": { "anyOf": [ { "type": "string" }, { "items": { "type": "string" }, "type": "array", "uniqueItems": true }, { "type": "null" } ], "default": null, "title": "Issues" }, "source-code": { "anyOf": [ { "format": "uri", "minLength": 1, "type": "string" }, { "type": "null" } ], "default": null, "title": "Source-Code" }, "license": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "License" }, "plugs": { "$ref": "#/$defs/Plugs", "default": {} }, "slots": { "$ref": "#/$defs/Slots", "default": {} }, "sdkcraft-started-at": { "title": "Sdkcraft-Started-At", "type": "string" } }, "required": [ "name", "version", "summary", "description", "architecture", "sdkcraft-started-at" ], "title": "Metadata", "type": "object" } ``` ## Examples In-project SDK that declares a mount plug: ```yaml name: ccache version: "0.1" summary: Shared ccache description: | Project-specific SDK that exposes a mount target for preserving cache across workshop updates. plugs: ccache: interface: mount workshop-target: /home/workshop/.cache/ccache ``` Runtime `sdk.yaml` written by **SDKcraft** for a Go development SDK: ```yaml name: go title: Go SDK version: "1.25.1" summary: The Go programming language description: | Go is an open source programming language that enables the production of simple, efficient and reliable software at scale. base: ubuntu@24.04 architecture: amd64 license: LGPL-2.1 sdkcraft-started-at: "2026-04-12T08:30:00Z" plugs: mod-cache: interface: mount workshop-target: /home/workshop/go/pkg/mod ``` ## See also Explanation: - [In-project SDKs](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-in-project-sdk) - [SDK concepts](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-concepts) - [SDK definition](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-definition) - [System SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-system-sdk) Reference: - [SDK internals](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-sdk-internals) - [SDKcraft project definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdkcraft-definition.md#ref-sdkcraft-definition) - [Workshop definition](https://ubuntu.com/workshop/docs//reference/definition-files/workshop-definition.md#ref-workshop-definition) Tutorial: - [Customize with sketch SDKs](https://ubuntu.com/workshop/docs//tutorial/part-3-sketch-sdks.md#tut-sketch-sdks) # sdk-vs-dockerfile.md # How SDKs compare to Dockerfiles **Workshop** didn’t occur in a vacuum; there have been many attempts to provide developers with robust environments. A common approach is to use Docker to achieve repeatability, persistence, layering, and various other benefits that the technology offers. We won’t dwell on the pros and cons here; instead, let’s discuss how a typical Dockerfile development environment maps to a workshop and its SDKs. #### NOTE We assume you’re familiar with **SDKcraft** basics covered in the [Craft SDKs with SDKcraft](https://ubuntu.com/workshop/docs//tutorial/part-4-craft-sdks.md#tut-craft-sdks) tutorial section and have an understanding of Docker. ## Feature discussion To begin with, it’s perfectly reasonable to draw a few comparisons between Docker and the combination of **Workshop** and **SDKcraft**. ### (Im)mutability The first contrast comes from the overall approach: Docker images are conceived to be immutable, whereas workshops are designed to evolve over time. This affects all aspects of their design and implementation, including how Dockerfiles and SDKs are laid out, respectively. ### Bind mounts and volumes Docker provides several ways to manage data persistence and storage such as the `VOLUME` instructions, the **docker volume** command or the `--mount` and `-v` options in **docker run**. The expectations for their configuration are set by the image author but the actual parameters are provided by users at the author’s guidance; the resulting manual process is error-prone and adds unnecessary overhead. **Workshop** and **SDKcraft** reciprocate this with [mount interface](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md#exp-mount-interface) plugs that are akin to Docker volumes and the **workshop remount** command that enables remounting existing plugs to a given location. However, the user can’t create arbitrary mounts; the choice is limited to what the SDKs offer. In turn, this implies that the mount logic in **Workshop** and **SDKcraft** is built into the SDK by its author, not implemented manually by the user; unless the user decides to intervene, the mounts are managed automatically and largely stay hidden. ### Resource usage For largely historical reasons, the Docker way of accessing various host resources can be notably inconsistent; for example, enabling GPU pass-through is visibly different from SSH forwarding. In contrast, **Workshop** and **SDKcraft** unify these mechanisms under the single concept of an [interface](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-concepts), providing a consistent way to uniformly manage host resource access. ### Parts and layers Docker relies on a temporally layered approach, where each change is built on top of the previous one. Our SDKs are structured using [parts](https://ubuntu.com/workshop/docs//explanation/sdks/parts.md#exp-sdk-parts); their expressiveness makes them more diverse and semantically rich, allowing the layout of an SDK to be formalized in a modular way. If necessary, the layered approach can be mimicked using [SDK hooks](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks). **Workshop** uses ZFS snapshots and clones to cache the results of each `setup-base` hook. ### Build commands In Docker, build commands are typically bundled as `RUN` instructions. In **SDKcraft** SDKs, the `setup-base` [hook](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks) is responsible for building the workshop, but other hooks add extra functionality with runtime events and health checks. A related Docker pattern applies `USER` to switch from root to a nonroot user, followed by `RUN` instructions for user-level setup: ```docker # System setup as root (≈ setup-base) RUN apt-get update && apt-get install -y ... # Switch to non-root user and set up the project (≈ setup-project) USER appuser WORKDIR /home/appuser RUN pip install --user ... ``` In **Workshop**, this maps to the `setup-project` [hook](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks), which inherently runs as the `workshop` user. Unlike `setup-base`, `setup-project` runs after interfaces are connected and the `/project/` directory is mounted, so it can leverage available hardware and install project-specific dependencies. ## Data persistence and sharing Consider this Docker command: ```console $ docker run --name share-example --entrypoint bash -it \ -v ~/docker/kit/cache/Kit:/kit/cache:rw \ -v ~/docker/cache/ov:/root/.cache/ov:rw \ ... ``` All too familiar, isn’t it? When running a sufficiently complex container, you need to mount a lot of directories to make it work, and the handling of these mounts both inside and outside the container can quickly become an overhead. **Workshop** addresses this issue by providing a way to reuse and share content between the host and the workshop via SDKs while keeping manual intervention to a necessary minimum. Typically, workshops are isolated from each other and from the host system; all data exchange is via the mount interface. To use this interface, your SDK defines a [mount interface plug](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md#exp-mount-plug). When a workshop uses the SDK, an auto-assigned, noncustomizable source directory on the host is mounted to the plug-defined target directory inside the workshop. What’s more, its contents are preserved during refresh operations. In this way, **Workshop** enables SDK data persistence and reuse *inside* individual workshops. Note, however, that files created in the plug’s target location by any means will only be accessible to the workshop to which that specific auto-assigned source directory is mounted to. Other workshops, even if they use the same SDK, cannot access these files and will not share them; their source directories will be different. ### Persistence and reuse between workshops This is the simplest scenario; you use the `mount` interface to define the target directory where the content will be mounted inside the workshop per each directory you want to retain during the workshop’s lifecycle. ```yaml name: data-science title: Data science SDK base: ubuntu@22.04 summary: This SDK does some data science. description: | Besides doing actual data science, this SDK demonstrates content sharing and persistence between workshops by enabling two plugs that can store reusable data specific to the SDK. plugs: share-cache: interface: mount workshop-target: /opt/cache training-data: interface: mount workshop-target: /opt/training read-only: true ``` This SDK defines two mount plugs; for each, **Workshop** creates a source directory on the host at runtime. Both `workshop-target` directories inside the workshop can be used by the SDK-specific logic implemented via [hooks](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks) and other features. Additionally, you can mark a directory as read-only. **Workshop** will then enforce the immutability of resources in this directory when they are accessed from inside the workshop. Here’s a corresponding workshop definition: ```yaml name: data base: ubuntu@22.04 sdks: - name: data-science ``` The default host location that **Workshop** mounts to the target is predefined as follows: ```none $XDG_DATA_HOME/workshop/id///mount/// ``` In the above example, this would be `~/.local/share/workshop/id///mount/data-science/share-cache/`. In particular, this means that the SDK’s plug in each workshop will have its own unique source directory. ### Share custom host content with a workshop One issue that the previous scenario doesn’t address is customizing the source directory of a plug. The **docker run** example at the beginning illustrates this approach; it explicitly lists the host directories to be mounted to each target. This can also be done with **Workshop**, and the **workshop remount** command is the key to it: ```console $ workshop remount data/data-science:share-cache ~/.local/cache/ ``` This mounts a specific source location on the host, `~/.local/cache/`, to the target directory of the `share-cache` mount interface plug under the `data-science` SDK in the `data` workshop defined above. ## Feature mapping Any attempt at a straightforward comparison of these different, albeit vaguely similar, technologies is mostly futile. Again, a key difference is that a Dockerfile is controlled by the user, but a workshop is *managed* by the user, yet it relies on publisher-defined SDKs whose layout is beyond the user’s reach. This means that some capabilities of Docker won’t be available to a user of **Workshop** alone, so the functionality is split between the user-oriented **Workshop** and the publisher-focused **SDKcraft**. Important Dockerfile instructions are mapped to **SDKcraft** as follows: | Dockerfile | SDKcraft | |---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `ADD` | [parts](https://ubuntu.com/workshop/docs//explanation/sdks/parts.md#exp-sdk-parts),
[mount interface](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md#exp-mount-interface) | | `CMD` | `setup-base` [hook](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks) | | `COPY` | `setup-base` [hook](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks) | | `ENTRYPOINT` | `setup-base` [hook](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks) | | `FROM` | `base` in the [SDK definition](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-definition) | | `HEALTHCHECK` | `check-health` hook | | `ONBUILD` | `setup-base` [hook](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks) | | `RUN` | `setup-base`,
`setup-project` [hooks](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks) | | `USER` | `setup-project` [hook](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks) | | `VOLUME` | [mount interface](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md#exp-mount-interface) | In turn, the CLI subcommands can be mapped like this: | Docker CLI | Workshop/SDKcraft CLI | |-------------------------------------------|---------------------------------------------------------| | **docker build** | **sdkcraft build**, **sdkcraft pack** | | **docker exec** | **workshop exec**, **workshop shell**, **workshop run** | | **docker images**, **docker ps** | **workshop info**, **workshop list** | | **docker logs** | **workshop changes**, **workshop tasks** | | **docker rm**, **docker rmi** | **workshop remove** | | **docker run** | **workshop launch**, **workshop refresh** | | **docker run --mount**, **docker volume** | **workshop remount** | | **docker start** | **workshop start** | | **docker stop** | **workshop stop** | ## Case study: ROS 2 For a specific example, consider the [Docker-based tutorial](https://docs.ros.org/en/jazzy/How-To-Guides/Setup-ROS-2-with-VSCode-and-Docker-Container.html) for ROS 2, the open-source robotics operating system. The choice is influenced by many factors, including the fact that we have a ROS 2 SDK available for comparison; for details, refer to the corresponding how-to guide under [See also](). Nonetheless, we won’t focus on the specifics of ROS 2 here; instead, we discuss how certain parts of an arbitrarily sophisticated Dockerfile map to a similar SDK and the workshop that uses it. ### Base image The example suggests using the `ros:rolling` tag for the [Dockerfile](https://docs.ros.org/en/jazzy/How-To-Guides/Setup-ROS-2-with-VSCode-and-Docker-Container.html#edit-dockerfile); with a few [levels of indirection](https://hub.docker.com/_/ros/), it comes down to this (or similar) instruction: ```docker FROM ubuntu:noble ``` For **Workshop** and **SDKcraft**, this translates to `ubuntu@24.04` in the [SDK definition](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-definition) and the [workshop definition](https://ubuntu.com/workshop/docs//reference/definition-files/workshop-definition.md#ref-workshop-definition). ### Project workspace The [project workspace](https://docs.ros.org/en/jazzy/How-To-Guides/Setup-ROS-2-with-VSCode-and-Docker-Container.html#configure-workspace-in-docker-and-vs-code) in the example is defined as a bind mount that eventually becomes this: ```console $ docker run -it \ --mount type=bind,source=/home/user/ros-project,target=/home/ws/src,consistency=cached \ # ... ``` Its counterpart in **Workshop** is the *project directory* where the workshop was defined and launched; it is automatically mounted as `/project/` when the workshop is started: ```console $ workshop launch ros2jazzy # must be run in the project directory ``` No explicit configuration is needed; this behavior is intentionally consistent across all workshops. ### Bind mounts The ROS 2 example defines a [few more mounts](https://docs.ros.org/en/jazzy/How-To-Guides/Setup-ROS-2-with-VSCode-and-Docker-Container.html#edit-devcontainer-json-for-your-environment); a complete **docker run** command may look like this: ```console $ docker run -it \ --name ros2_container \ --mount type=bind,source=/home/user/ros-project,target=/home/ws/src,consistency=cached \ --mount type=bind,source=/home/user/.ros,target=/root/.ros,consistency=cached \ --mount type=bind,source=/tmp/.X11-unix,target=/tmp/.X11-unix,consistency=cached \ --mount type=bind,source=/dev/dri,target=/dev/dri,consistency=cached \ ros2 ``` In **Workshop** and **SDKcraft**, additional filesystem mounts are defined by the SDK author or the user using the [mount interface](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md#exp-mount-interface): ```yaml plugs: ros-cache: interface: mount workshop-target: /home/workshop/.ros # ... ``` Just like with the [project files](#exp-docker-project), this avoids the need for manual setup when starting the workshop: ```console $ workshop launch ros2jazzy # the plugs are mounted automatically ``` Again, **Workshop** and **SDKcraft** have no direct counterpart to bind mounts; plugs are more similar to Docker volumes. Yet, the **workshop remount** command enables remounting existing plugs to new host directories: ```console $ workshop remount ros2jazzy/ros2:ros-cache ~/new-cache-mount/ ``` Thus, **Workshop** and **SDKcraft** largely leave the design of mount points to the SDK author, allowing the user to rely on their default, well-defined behavior with the extra option of adjusting them if necessary. ### Build commands Normally, a `RUN` instruction in a Dockerfile translates to the `setup-base` and `setup-project` [hooks](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks) in an SDK pretty well. Here, the steps to [set up keys](https://github.com/osrf/docker_images/blob/7f98ddd88d872299c45b60c8bcd70d4eb6665222/ros/rolling/ubuntu/noble/ros-core/Dockerfile#L19), then [configure the repos](https://github.com/osrf/docker_images/blob/7f98ddd88d872299c45b60c8bcd70d4eb6665222/ros/rolling/ubuntu/noble/ros-core/Dockerfile#L29) and [install the packages](https://github.com/osrf/docker_images/blob/7f98ddd88d872299c45b60c8bcd70d4eb6665222/ros/rolling/ubuntu/noble/ros-core/Dockerfile#L38) largely stay the same. However, `setup-project` runs with the project directory already mounted, so any steps that rely on the contents of the project itself can be implemented with the same hook. In particular, this enables the ROS 2 SDK to transparently identify and install project-specific dependencies. ## See also Explanation: - [Mount interface](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md#exp-mount-interface) - [Projects](https://ubuntu.com/workshop/docs//explanation/workshops/projects.md#exp-projects) - [SDKs](https://ubuntu.com/workshop/docs//explanation/index.md#exp-sdks) Reference: - [Mount interface](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-mount-interface) - [SDK definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdk-definition.md#ref-sdk-definition) - [SDK hooks](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-sdk-hooks) # sdk.md # sdk (CLI) The **sdk** utility exposes the following commands, each with its own set of options, and also has a number of global flags: ## sdk find Search the Store for SDKs. ### Usage ```console $ sdk find [flags] ``` ### Description Search the Store for SDKs matching the given query. The query can match the SDK’s name, title, summary, description, or publisher. Notes: - Only the latest release of the SDK is shown. - To view more details for one of the SDKs, use “sdk info”. - To list SDKs on the local system, use “sdk list”. ### Examples Search for SDKs matching a single keyword: ```console $ sdk find openvino ``` Combine multiple words into a single query: ```console $ sdk find jupyter notebooks ``` Hide the table header in the output: ```console $ sdk find openvino --no-headers ``` ### Flags ## sdk info Show SDK info. ### Usage ```console $ sdk info [flags] ``` ### Description Prints the SDK’s metadata, shows the revisions currently available in the SDK Store, and lists workshops where the SDK is installed. Notes: - The output shows the SDK’s build date. - For an overview of SDK volumes, use “sdk list”. - For per-workshop information, use “workshop info”. ### Examples Show metadata, Store channels, and local installations for the “openvino” SDK: ```console $ sdk info openvino ``` Restrict the Store channels to a specific base: ```console $ sdk info openvino --base ubuntu@24.04 ``` Show the channels for every supported architecture: ```console $ sdk info openvino --arch all ``` ### Flags ## sdk list List SDK volumes available on this machine. ### Usage ```console $ sdk list [flags] ``` ### Description This command lists all local SDK volumes. Use it to enumerate the SDK revisions currently stored on the system. Only volumes are reported, not the workshops that use them. Notes: - For per-workshop information, use “workshop info”. - Multiple entries may appear for a single SDK if several revisions are present simultaneously. ### Examples List all local SDK volumes: ```console $ sdk list ``` Hide the table header in the output: ```console $ sdk list --no-headers ``` ### Flags ## Shell completion The **sdk** CLI ships completion scripts for Bash, Zsh, and Fish. #### NOTE When **Workshop** is installed via snap, completion for Bash, Zsh, and Fish is enabled automatically for both **workshop** and **sdk**; no further configuration is needed for these shells. To enable completion for the current shell session, source the script for your shell. Bash: ```console $ source <(sdk completion bash) ``` Zsh: ```console $ source <(sdk completion zsh) ``` Fish: ```console $ sdk completion fish | source ``` For per-shell installation that persists across new sessions, follow the instructions printed by the shell-specific help command. For example, for Bash: ```console $ sdk completion bash --help ``` ## See also Explanation: - [sdk (CLI)](https://ubuntu.com/workshop/docs//explanation/sdks/sdk-cli.md#exp-sdk-cli) # sdkcraft-cli.md # sdkcraft (CLI) **SDKcraft** is the SDK-author tool, shipped as its own snap. While **workshop** and **sdk** are aimed at workshop users and **workshopctl** runs inside workshops, **sdkcraft** is what an SDK publisher uses on their workstation to scaffold, build, test, try, and publish SDKs to the SDK Store. There are several categories of commands that vary by their purpose: | Actions | Commands | What they do | |---------------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------| | Lifecycle | **build**,
**clean**,
**pack**,
**prime**,
**pull**,
**stage**,
**test**,
**try** | Work through the parts-based build pipeline
to produce an SDK artefact
and try it locally before publishing. | | Store | **create-track**,
**register**,
**release**,
**upload** | Claim an SDK name, manage its tracks,
upload artefacts, and release revisions to channels. | | Store account | **login**,
**whoami** | Authenticate against the SDK Store
and inspect the active identity. | | Other | **init**,
**version** | Bootstrap a new project layout
and report the installed **SDKcraft** version. | #### NOTE **SDKcraft** is a separate snap, installed independently of **Workshop**. See the [SDKcraft](https://github.com/canonical/sdkcraft/) repository for installation and release notes. ## See also Explanation: - [sdk (CLI)](https://ubuntu.com/workshop/docs//explanation/sdks/sdk-cli.md#exp-sdk-cli) - [workshop (CLI)](https://ubuntu.com/workshop/docs//explanation/workshops/workshop-cli.md#exp-workshop-cli) Reference: - [Command-line interfaces](https://ubuntu.com/workshop/docs//reference/index.md#ref-cli) Tutorial: - [Get started with workshops](https://ubuntu.com/workshop/docs//tutorial/part-1-get-started.md#tut-get-started) # sdkcraft-definition.md # SDKcraft project definition The `sdkcraft.yaml` file is the *build-time* SDK definition: **SDKcraft** reads it to pack an SDK. SDK publishers author this file; **SDKcraft** writes the runtime `sdk.yaml` (see [SDK definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdk-definition.md#ref-sdk-definition)) into the resulting package, copying plug, slot, and metadata fields across. **SDKcraft** builds on the [craft-application](https://canonical-craft-application.readthedocs-hosted.com/) framework and [craft-parts](https://canonical-craft-parts.readthedocs-hosted.com/) for build orchestration. Many fields are inherited from `craft-application`. ## Filename and location - The definition file is `sdkcraft.yaml` or `.sdkcraft.yaml` at the project root. - Hooks live next to it under `hooks/`; **SDKcraft** lints them with [ShellCheck](https://www.shellcheck.net/) and packs them with the SDK. ## Top-level fields | Key | Value | Description | |------------------------|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `name` (required) | string | SDK identifier. Must contain at least one lowercase letter
and may consist of lowercase letters, digits, and hyphens between them.
Up to 40 characters.
Cannot be `agent`, `system`, `sketch`,
or start with `try-` or `project-`; those names are reserved. | | `platforms` (required) | object | Platforms the SDK can be built on and for.
See [Platform entry](#ref-sdkcraft-definition-platforms). | | `base` | string | Base operating system image the SDK targets at runtime.
One of `ubuntu@20.04`, `ubuntu@22.04`, `ubuntu@24.04`,
or `ubuntu@26.04`.
Omit for SDKs that work on any supported base. | | `build-base` | string | Base operating system image used to build the SDK.
Required when `base` is omitted. | | `version` | string | SDK version. Semantic versioning is recommended.

#### NOTE
Quote version strings in YAML when they look numeric
(for example, `version: "1.0"`). | | `title` | string | Human-readable title. | | `summary` | string | One-line summary, up to 78 characters. | | `description` | string | Longer free-form description, up to about a hundred words. | | `license` | string | License name, as it would appear in package metadata.
Match the license to the actual components the SDK installs. | | `contact` | string, array, or URL | Contact information for the SDK publisher. | | `issues` | string, array, or URL | Where users should report problems with the SDK. | | `source-code` | URL | Where the SDK’s source code is hosted. | | `adopt-info` | string | Name of a part whose `craftctl set` commands provide
`version` or `summary` at build time.
Standard `craft-application` machinery. | | `package-repositories` | array | Additional package repositories to enable while building.
Standard `craft-application` machinery;
see the [craft-archives reference](https://canonical-craft-archives.readthedocs-hosted.com/). | | `parts` | object | Build instructions, in craft-parts format.
See [Part entry](#ref-sdkcraft-definition-parts). | | `plugs` | object | Plugs the SDK requests from the workshop environment.
See [Interfaces](#ref-sdkcraft-definition-interfaces). | | `slots` | object | Slots the SDK provides.
Only the `mount` and `tunnel` interfaces support slots here.
See [Interfaces](#ref-sdkcraft-definition-interfaces). | **SDKcraft** writes `name`, `base`, `version`, `title`, `summary`, `description`, `license`, `contact`, `issues`, `source-code`, `plugs`, and `slots` straight into the runtime `sdk.yaml`. The other top-level fields control the build only. ## Nested structures ### Platform entry Each entry under `platforms` declares one build target. The key is the platform name; the value is an object: | Key | Value | Description | |------------------------|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| | `build-on` (required) | string or array of strings | Architectures or `:` triples
on which **SDKcraft** may build this platform.
Entries are unique; at least one is required. | | `build-for` (required) | string or array of strings | Architectures or `:` triples this build targets.
Use `all` for SDKs that ship no compiled binaries. | The platform name may be shorthand for both `build-on` and `build-for` (for example, a key of `amd64` with no nested value). ### Part entry Each entry under `parts` is a craft-parts definition: a key naming the part, with a value that specifies a `plugin` and the plugin’s parameters. **SDKcraft** forbids `stage-packages` and `stage-snaps` in parts; install packages and snaps from the `setup-base` hook instead. When `parts` is omitted, **SDKcraft** supplies a default part equivalent to `*default-part: {plugin: nil*}`. For the full set of plugin types, lifecycle steps, and override mechanisms, see the [craft-parts reference](https://documentation.ubuntu.com/craft-parts/latest/reference/parts_steps/). ## Interfaces Plug and slot values in `sdkcraft.yaml` use the same shape as in `sdk.yaml`. A plug or slot value is an inline definition: a mapping that specifies the `interface` and any interface-specific attributes. ### Camera interface The camera interface exposes a host camera device. - Plug attributes: none. - Plug name: must be `camera`. - Plug owner: any regular SDK; not the system SDK. - Slot: the system SDK provides a single `system:camera` slot. Other SDKs cannot declare camera slots. ### Custom device interface The custom device interface exposes host devices that belong to a Linux kernel subsystem. A custom device plug is described by this attribute: | Key | Value | Description | |------------------------|---------|-----------------------------------------------------------------------------------------------------| | `subsystem` (required) | string | The Linux kernel subsystem of the host devices to expose,
for example `input`, `tty`, or `usb`. | Plug owner: any regular SDK; not the system SDK. Slot: the system SDK provides a single `system:custom-device` slot. Other SDKs cannot declare custom device slots. ### Desktop interface The desktop interface exposes the host display server. - Plug attributes: none. - Plug name: must be `desktop`. - Plug owner: any regular SDK; not the system SDK. - Slot: the system SDK provides a single `system:desktop` slot. Other SDKs cannot declare desktop slots. ### GPU interface The GPU interface exposes host GPU devices. - Plug attributes: none. - Plug name: must be `gpu`. - Plug owner: any regular SDK; not the system SDK. - Slot: the system SDK provides a single `system:gpu` slot. Other SDKs cannot declare GPU slots. ### Mount interface The mount interface exposes a directory between a slot owner and a plug owner. A mount plug is described by these attributes: | Key | Value | Description | |------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `workshop-target` (required) | string | Path inside the workshop used as the plug’s target directory.
Must be an absolute path;
`$SDK` expands to the SDK’s installation path in the workshop. | | `mode` | integer | File permissions, in octal, applied when creating `workshop-target`
and any missing parent directories.
Defaults to `0o775` for regular users.
When `uid` is zero, defaults to `0o755`. | | `uid` | integer | User ID applied when creating `workshop-target`
and any missing parent directories.
Defaults to `1000` when `workshop-target` is under
`/home/workshop/`, `/project/`, or `/run/user/1000/`.
Defaults to `0` otherwise. | | `gid` | integer | Group ID applied when creating `workshop-target`
and any missing parent directories.
Defaults to `1000` or `0`
by the same path rule as `uid`,
even when `uid` is set explicitly. | | `read-only` | Boolean | Whether the target directory should be read-only.
Defaults to `false`. | Plug owner: any regular SDK; not the system SDK. The system SDK provides one mount slot, `system:mount`, with a dynamic `host-source` attribute that can be configured only at [remount](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-remount). It is the only mount slot whose source is on the host filesystem. A mount slot on a regular SDK is described by this attribute: | Key | Value | Description | |------------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| | `workshop-source` (required) | string | Path inside the workshop used as the slot’s source directory.
Must be an absolute path;
`$SDK` expands to the SDK’s installation path in the workshop. | ### SSH interface The SSH interface exposes the user’s SSH agent socket. - Plug attributes: none. - Plug name: must be `ssh-agent`. - Plug owner: any regular SDK; not the system SDK. - Slot: the system SDK provides a single `system:ssh-agent` slot. Other SDKs cannot declare SSH slots. ### Tunnel interface The tunnel interface forwards a network address or Unix domain socket. Both tunnel plugs and tunnel slots take a single attribute: | Key | Value | Description | |------------|---------|-----------------------------------------------------------------------------------------------------------------------------------| | `endpoint` | string | Network address or Unix domain socket that forms one end of the tunnel.
Defaults to `localhost/tcp` for both plugs and slots. | The `endpoint` value follows this grammar: | Field | Format | |----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Endpoint | `
/` for network endpoints;
may be shortened to `
` or `` alone.

`` or `@` for Unix domain sockets. | | Address | `:`; may be shortened to `` or ``. | | Protocol | Either `tcp` or `udp`. Defaults to `tcp`. | | Host | An IPv4 or IPv6 address.
When a port is supplied, IPv6 addresses must be enclosed in square brackets.

Supported aliases: `localhost`, `ip6-localhost`, and `ip6-loopback`.
Defaults to `localhost`. | | Port | A TCP or UDP port number (1-65535).
May be omitted, but only on one side of a connection; both sides then use the same port.

For security, tunnel plugs in the system SDK cannot use privileged ports (1-1023). | | Path | Absolute path to a Unix domain socket.

`$HOME` expands to the user’s home directory
and `$XDG_RUNTIME_DIR` expands to the user runtime directory
(typically `/run/user/1000`).

For security, tunnel plugs in the system SDK cannot listen on sockets outside these two directories. | | String | An abstract socket name. | Endpoints that start with `[` or `@` must be quoted in YAML: ```yaml endpoint: '[::1]:8080/tcp' endpoint: '@abstract.sock' ``` ## JSON Schema The following JSON Schema is exported from **SDKcraft**’s project model and describes the structure above: #### NOTE Numeric bounds use pydantic-style `ge`, `le`, and `lt` keywords. Generic JSON Schema validators will not enforce them; treat the bounds as documentation of the accepted ranges, and rely on the field table above for the authoritative rules. ### SDKcraft project schema ```json { "$defs": { "CameraPlug": { "additionalProperties": false, "description": "SDKcraft project camera plug definition.", "properties": { "interface": { "const": "camera", "title": "Interface", "type": "string" } }, "required": [ "interface" ], "title": "CameraPlug", "type": "object" }, "CleanAbsPath": { "type": "string" }, "CustomDevicePlug": { "additionalProperties": false, "description": "SDKcraft project custom-device plug definition.", "properties": { "interface": { "const": "custom-device", "title": "Interface", "type": "string" }, "subsystem": { "description": "Device subsystem.", "examples": [ "accel", "usb" ], "minLength": 1, "title": "Subsystem", "type": "string" } }, "required": [ "interface", "subsystem" ], "title": "CustomDevicePlug", "type": "object" }, "DesktopPlug": { "additionalProperties": false, "description": "SDKcraft project desktop plug definition.", "properties": { "interface": { "const": "desktop", "title": "Interface", "type": "string" } }, "required": [ "interface" ], "title": "DesktopPlug", "type": "object" }, "Endpoint": { "type": "string" }, "FileMode": { "$ref": "#/$defs/Int", "ge": 0, "le": 511 }, "GPUPlug": { "additionalProperties": false, "description": "SDKcraft project GPU plug definition.", "properties": { "interface": { "const": "gpu", "title": "Interface", "type": "string" } }, "required": [ "interface" ], "title": "GPUPlug", "type": "object" }, "Int": { "type": "integer" }, "MountPlug": { "additionalProperties": false, "description": "SDKcraft project mount plug definition.", "properties": { "interface": { "const": "mount", "title": "Interface", "type": "string" }, "workshop-target": { "$ref": "#/$defs/CleanAbsPath" }, "uid": { "$ref": "#/$defs/UserGroupID" }, "gid": { "$ref": "#/$defs/UserGroupID" }, "mode": { "$ref": "#/$defs/FileMode" }, "read-only": { "default": false, "title": "Read-Only", "type": "boolean" } }, "required": [ "interface", "workshop-target" ], "title": "MountPlug", "type": "object" }, "MountSlot": { "additionalProperties": false, "description": "SDKcraft project mount slot definition.", "properties": { "interface": { "const": "mount", "title": "Interface", "type": "string" }, "workshop-source": { "$ref": "#/$defs/CleanAbsPath" } }, "required": [ "interface", "workshop-source" ], "title": "MountSlot", "type": "object" }, "Part": { "additionalProperties": true, "type": "object" }, "Platform": { "additionalProperties": false, "description": "A single platform entry in the platforms dictionary.\n\nThis model defines how a single value under the ``platforms`` key works for a project.", "properties": { "build-on": { "anyOf": [ { "items": { "type": "string" }, "type": "array", "uniqueItems": true }, { "type": "string" } ], "examples": [ "amd64", [ "arm64", "riscv64" ] ], "minLength": 1, "title": "Build-On" }, "build-for": { "anyOf": [ { "items": { "type": "string" }, "maxItems": 1, "minItems": 1, "type": "array" }, { "type": "string" } ], "examples": [ "amd64", [ "riscv64" ] ], "title": "Build-For" } }, "required": [ "build-on", "build-for" ], "title": "Platform", "type": "object" }, "Plug": { "discriminator": { "mapping": { "camera": "#/$defs/CameraPlug", "custom-device": "#/$defs/CustomDevicePlug", "desktop": "#/$defs/DesktopPlug", "gpu": "#/$defs/GPUPlug", "mount": "#/$defs/MountPlug", "ssh-agent": "#/$defs/SSHAgentPlug", "tunnel": "#/$defs/TunnelPlug" }, "propertyName": "interface" }, "oneOf": [ { "$ref": "#/$defs/CameraPlug" }, { "$ref": "#/$defs/CustomDevicePlug" }, { "$ref": "#/$defs/DesktopPlug" }, { "$ref": "#/$defs/GPUPlug" }, { "$ref": "#/$defs/MountPlug" }, { "$ref": "#/$defs/SSHAgentPlug" }, { "$ref": "#/$defs/TunnelPlug" } ] }, "PlugName": { "description": "The name of the plug. This is used when connecting and disconnecting.\n\nThe plug name must consist only of lower-case ASCII letters (``a-z``), numerals\n(``0-9``), and hyphens (``-``). It must start with a letter, not end with a\nhyphen, and not contain two consecutive hyphens.\n", "examples": [ "desktop", "gpu", "ssh-agent" ], "pattern": "^[a-z](-?[a-z0-9])*$", "title": "Plug Name", "type": "string" }, "Plugs": { "additionalProperties": { "$ref": "#/$defs/Plug" }, "propertyNames": { "$ref": "#/$defs/PlugName" }, "type": "object" }, "ProjectName": { "description": "The name of the project. This is used when uploading, publishing, or installing.\n\nThe project name must consist only of lower-case ASCII letters (``a``-``z``), numerals\n(``0``-``9``), and hyphens (``-``). It must contain at least one letter, not start or\nend with a hyphen, and not contain two consecutive hyphens. The maximum length is 40\ncharacters.\n", "examples": [ "ubuntu", "jupyterlab-desktop", "lxd", "digikam", "kafka", "mysql-router-k8s" ], "maxLength": 40, "minLength": 1, "pattern": "(?!^(system|try-.*|project-.*|sketch)$)^([a-z0-9][a-z0-9-]?)*[a-z]+([a-z0-9-]?[a-z0-9])*$", "title": "Project Name", "type": "string" }, "SSHAgentPlug": { "additionalProperties": false, "description": "SDKcraft project SSH agent plug definition.", "properties": { "interface": { "const": "ssh-agent", "title": "Interface", "type": "string" } }, "required": [ "interface" ], "title": "SSHAgentPlug", "type": "object" }, "Slot": { "discriminator": { "mapping": { "mount": "#/$defs/MountSlot", "tunnel": "#/$defs/TunnelSlot" }, "propertyName": "interface" }, "oneOf": [ { "$ref": "#/$defs/MountSlot" }, { "$ref": "#/$defs/TunnelSlot" } ] }, "SlotName": { "description": "The name of the slot. This is used when connecting and disconnecting.\n\nThe slot name must consist only of lower-case ASCII letters (``a-z``), numerals\n(``0-9``), and hyphens (``-``). It must start with a letter, not end with a\nhyphen, and not contain two consecutive hyphens.\n", "examples": [ "dashboard", "gdb", "toolchain" ], "pattern": "^[a-z](-?[a-z0-9])*$", "title": "Slot Name", "type": "string" }, "Slots": { "additionalProperties": { "$ref": "#/$defs/Slot" }, "propertyNames": { "$ref": "#/$defs/SlotName" }, "type": "object" }, "TunnelPlug": { "additionalProperties": false, "description": "SDKcraft project tunnel plug definition.", "properties": { "interface": { "const": "tunnel", "title": "Interface", "type": "string" }, "endpoint": { "$ref": "#/$defs/Endpoint", "default": "" } }, "required": [ "interface" ], "title": "TunnelPlug", "type": "object" }, "TunnelSlot": { "additionalProperties": false, "description": "SDKcraft project tunnel plug definition.", "properties": { "interface": { "const": "tunnel", "title": "Interface", "type": "string" }, "endpoint": { "$ref": "#/$defs/Endpoint", "default": "" } }, "required": [ "interface" ], "title": "TunnelSlot", "type": "object" }, "UserGroupID": { "$ref": "#/$defs/Int", "ge": 0, "lt": 4294967295 } }, "additionalProperties": false, "description": "SDKcraft project definition.", "properties": { "name": { "$ref": "#/$defs/ProjectName" }, "title": { "anyOf": [ { "description": "A human-readable title.", "examples": [ "Ubuntu Linux", "Jupyter Lab Desktop", "LXD", "DigiKam", "Apache Kafka", "MySQL Router K8s charm" ], "maxLength": 40, "minLength": 2, "title": "Title", "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "version": { "anyOf": [ { "description": "The version of the project, enclosed in quotation marks.", "examples": [ "\"0.1\"", "\"1.0.0\"", "\"v1.0.0\"", "\"24.04\"" ], "maxLength": 32, "title": "version string", "type": "string" }, { "type": "null" } ], "default": null, "title": "Version" }, "summary": { "anyOf": [ { "description": "A short description of the project. Maximum length 78 characters.", "examples": [ "Linux for Human Beings", "The cross-platform desktop application for JupyterLab", "Container and VM manager", "Photo Management Program", "Charm for routing MySQL databases in Kubernetes", "An open-source event streaming platform for high-performance data pipelines" ], "maxLength": 78, "title": "Summary", "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "description": "The full description of the project.", "title": "Description" }, "base": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Base" }, "build-base": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Build-Base" }, "platforms": { "additionalProperties": { "$ref": "#/$defs/Platform" }, "description": "Determines which architectures the project builds and runs on.", "examples": [ "{amd64: {build-on: [amd64], build-for: [amd64]}, arm64: {build-on: [amd64, arm64], build-for: [arm64]}}" ], "patternProperties": { "(amd64|arm64|armhf|i386|ppc64el|riscv64|s390x)": { "anyOf": [ { "type": "null" }, { "properties": { "build-on": { "anyOf": [ { "type": "string" }, { "items": { "type": "string" }, "type": "array" } ], "title": "Build-On" }, "build-for": { "anyOf": [ { "type": "string" }, { "items": { "type": "string" }, "type": "array" } ], "title": "Build-For" } }, "required": [ "build-on" ], "type": "object" } ] } }, "propertyNames": { "description": "The name of this platform. May not contain '/'", "examples": [ "riscv64", "my-special-platform" ], "not": { "enum": [ "*", "any" ] } }, "title": "Platforms", "type": "object" }, "contact": { "anyOf": [ { "type": "string" }, { "items": { "type": "string" }, "type": "array", "uniqueItems": true }, { "type": "null" } ], "default": null, "description": "The author's contact links and email addresses.", "examples": [ "[contact@example.com, https://example.com/contact]" ], "title": "Contact" }, "issues": { "anyOf": [ { "type": "string" }, { "items": { "type": "string" }, "type": "array", "uniqueItems": true }, { "type": "null" } ], "default": null, "description": "The links and email addresses for submitting issues, bugs, and feature requests.", "examples": [ "[issues@example.com, https://example.com/issues]" ], "title": "Issues" }, "source-code": { "anyOf": [ { "format": "uri", "minLength": 1, "type": "string" }, { "type": "null" } ], "default": null, "description": "The links to the source code of the project.", "examples": [ "[https://github.com/canonical/craft-application]" ], "title": "Source-Code" }, "license": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "description": "The project's license as an SPDX expression", "examples": [ "GPL-3.0+", "Apache-2.0" ], "title": "License" }, "adopt-info": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "description": "Selects a part to inherit metadata from.", "examples": [ "foo-part" ], "title": "Adopt-Info" }, "parts": { "additionalProperties": { "$ref": "#/$defs/Part" }, "default": { "default-part": { "plugin": "nil" } }, "title": "Parts", "type": "object" }, "package-repositories": { "anyOf": [ { "items": { "additionalProperties": true, "type": "object" }, "type": "array" }, { "type": "null" } ], "default": null, "description": "The package repositories to use for build and stage packages.", "examples": [ "[{type: apt, components: [main], suites: [xenial], key-id: 78E1918602959B9C59103100F1831DDAFC42E99D, url: http://ppa.launchpad.net/snappy-dev/snapcraft-daily/ubuntu}]" ], "title": "Package-Repositories" }, "plugs": { "$ref": "#/$defs/Plugs", "default": {} }, "slots": { "$ref": "#/$defs/Slots", "default": {} } }, "required": [ "name", "platforms" ], "title": "Project", "type": "object" } ``` ## Examples Complex SDK that uses `platforms`, `parts`, and a mix of plugs: ```yaml name: go build-base: ubuntu@24.04 title: Go SDK summary: The Go programming language description: | Go is an open source programming language that enables the production of simple, efficient and reliable software at scale. version: "1.25.1" license: LGPL-2.1 platforms: amd64: build-on: [amd64] build-for: [amd64] arm64: build-on: [amd64] build-for: [arm64] riscv64: build-on: [amd64] build-for: [riscv64] plugs: mod-cache: interface: mount workshop-target: /home/workshop/go/pkg/mod parts: go: plugin: dump source: https://go.dev/dl/go$CRAFT_PROJECT_VERSION.linux-$CRAFT_ARCH_BUILD_FOR.tar.gz source-type: tar ``` Multi-base SDK with no parts: ```yaml name: multibase version: "0.1" summary: Multibase SDK description: | This is my multibase SDK description. license: GPL-3.0 platforms: noble: build-on: [ubuntu@24.04:amd64, ubuntu@24.04:arm64] build-for: ubuntu@24.04:all jammy: build-on: [ubuntu@22.04:amd64, ubuntu@22.04:arm64] build-for: ubuntu@22.04:all ``` SDK that exposes mount and GPU plugs: ```yaml name: ros2 title: The ROS 2 SDK base: ubuntu@24.04 version: "0.1" summary: The strictly necessary ROS 2 development environment for your project. description: | The ROS 2 SDK creates a minimum viable development environment for your ROS 2 project. It sets up a bare-bones ROS 2 workspace before installing all of the dependencies for the ROS 2 project mounted by workshop. A developer can then connect to the workshop and immediately build the project. license: LGPL-2.1 platforms: amd64: arm64: plugs: ros-cache: interface: mount workshop-target: /home/workshop/.ros colcon-artifacts: interface: mount workshop-target: /home/workshop/colcon gpu: interface: gpu ``` ## See also Explanation: - [In-project SDKs](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-in-project-sdk) - [SDK concepts](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-concepts) - [SDK definition](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-definition) - [SDK hooks](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks) - [System SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-system-sdk) Reference: - [SDK definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdk-definition.md#ref-sdk-definition) - [Workshop definition](https://ubuntu.com/workshop/docs//reference/definition-files/workshop-definition.md#ref-workshop-definition) Tutorial: - [Craft SDKs with SDKcraft](https://ubuntu.com/workshop/docs//tutorial/part-4-craft-sdks.md#tut-craft-sdks) # sdkcraft.md # sdkcraft (CLI) The **sdkcraft** utility exposes the following commands, each with its own set of options, and also has a number of global flags: ## sdkcraft clean Remove a part’s assets ### Usage ```console $ sdkcraft clean [--destructive-mode] [--platform name] [part-name ...] ``` ### Description Clean up artifacts belonging to parts. If no parts are specified, remove the packing environment. ### Flags ### Examples Clean build artifacts: ```console $ sdkcraft clean ``` Clean specific parts: ```console $ sdkcraft clean my-part ``` Clean in destructive mode: ```console $ sdkcraft clean --destructive-mode ``` ## sdkcraft pull Download or retrieve artifacts defined for a part ### Usage ```console $ sdkcraft pull [--destructive-mode | --use-lxd] [--shell | --shell-after] [--debug] [--platform name | --build-for arch] [part-name ...] ``` ### Description Download or retrieve artifacts defined for a part. If part names are specified only those parts will be pulled, otherwise all parts will be pulled. ### Flags ## sdkcraft build Build artifacts defined for a part ### Usage ```console $ sdkcraft build [--destructive-mode | --use-lxd] [--shell | --shell-after] [--debug] [--platform name | --build-for arch] [part-name ...] ``` ### Description Build artifacts defined for a part. If part names are specified only those parts will be built, otherwise all parts will be built. ### Flags ## sdkcraft stage Stage built artifacts into a common staging area ### Usage ```console $ sdkcraft stage [--destructive-mode | --use-lxd] [--shell | --shell-after] [--debug] [--platform name | --build-for arch] [part-name ...] ``` ### Description Stage built artifacts into a common staging area. If part names are specified only those parts will be staged. The default is to stage all parts. ### Flags ## sdkcraft prime Prime artifacts defined for a part ### Usage ```console $ sdkcraft prime [--destructive-mode | --use-lxd] [--shell | --shell-after] [--debug] [--platform name | --build-for arch] [part-name ...] ``` ### Description Prepare the final payload to be packed, performing additional processing and adding metadata files. If part names are specified only those parts will be primed. The default is to prime all parts. ### Flags ## sdkcraft pack Create the final artifact ### Usage ```console $ sdkcraft pack [--destructive-mode] [--shell | --shell-after] [--debug] [--platform name | --build-for arch] [--output OUTPUT] ``` ### Description Process parts and create the final artifact. ### Flags ### Examples Pack the project: ```console $ sdkcraft pack ``` Pack to a specific output directory: ```console $ sdkcraft pack --output dist/ ``` ## sdkcraft test Run SDK tests ### Usage ```console $ sdkcraft test [--destructive-mode] [--shell | --shell-after] [--debug] [--platform name] [--list] [test_expressions ...] ``` ### Description Tests are defined and run using spread ([https://github.com/canonical/spread](https://github.com/canonical/spread)). Compared to running spread manually, sdkcraft test also: - Packs SDKs for all platforms matching the current architecture. - Copies the packed SDKs into the test environment using sdkcraft try. - Installs Workshop in the test environment. - Skips spread variants for bases that weren’t packed. ### Flags ### Examples Test the project: ```console $ sdkcraft test ``` List the jobs that would run: ```console $ sdkcraft test --list ``` Run a specific test suite: ```console $ sdkcraft test my-suite/ ``` ## sdkcraft try Try SDKs before publishing ### Usage ```console $ sdkcraft try [--destructive-mode] [--shell | --shell-after] [--debug] [--platform name | --build-for arch] [--output OUTPUT] [SDKs ...] ``` ### Description Pack the SDK and copy it to the Workshop try area. ### Flags ### Examples Try the built artifact: ```console $ sdkcraft try ``` ## sdkcraft init Initialize an SDKcraft project ### Usage ```console $ sdkcraft init [--name NAME] [--profile {simple}] [project_dir] ``` ### Description Initialize an SDKcraft project by creating an ‘sdkcraft.yaml’ file together with hooks and tests. ### Flags ### Examples Initialize a new project: ```console $ sdkcraft init ``` ## sdkcraft version Show the application version and exit ### Usage ```console $ sdkcraft version ``` ### Description Show the application version and exit ## sdkcraft create-track Create one or more tracks for an SDK on the SDK Store ### Usage ```console $ sdkcraft create-track --track TRACKS SDK ``` ### Description Create one or more tracks for an SDK on the SDK Store. The command lists all tracks it created. Tracks must match an existing guardrail for this SDK. ### Flags ### Examples Create two tracks for the “go” SDK: ```console $ sdkcraft create-track go --track 1.26 --track 1.25 ``` ## sdkcraft register Register an SDK name on the store ### Usage ```console $ sdkcraft register SDK ``` ### Description Register an SDK name on the SDK Store. This reserves the SDK name for your account, allowing you to upload revisions under that name. SDK names must be registered before uploading. ## sdkcraft release Release an SDK revision to store channels ### Usage ```console $ sdkcraft release SDK REVISION CHANNELS ``` ### Description Release at to the selected store . is a comma-separated list of valid channels on the store. The must exist on the store; to see available revisions, run sdkcraft revisions . The channel map is displayed after the operation. The format for a channel is [/][/], where: - is used to have long-term release channels. - can only be stable, candidate, beta, or edge. - is optional and dynamically creates a channel with a one-month expiration. ### Examples Release revision 8 to stable: ```console $ sdkcraft release my-sdk 8 stable ``` Release revision 8 to latest/stable: ```console $ sdkcraft release my-sdk 8 latest/stable ``` Release revision 9 to multiple channels: ```console $ sdkcraft release my-sdk 9 beta,edge ``` ## sdkcraft revisions List SDK revisions available on the store ### Usage ```console $ sdkcraft revisions SDK ``` ### Description List all available channels and revisions for from the store. Use this command to find the revision number to pass to sdkcraft release . ### Examples List revisions for an SDK: ```console $ sdkcraft revisions my-sdk ``` ## sdkcraft upload Upload an SDK artifact to the store ### Usage ```console $ sdkcraft upload [--release CHANNELS] SDK ``` ### Description Upload an SDK artifact to the SDK Store. The artifact must be a .sdk file created by the pack command. Optionally, the uploaded revision can be released to specified channels. ### Flags ## sdkcraft login Log in to the SDK Store ### Usage ```console $ sdkcraft login ``` ### Description Log in to the SDK Store. The login command requires a working keyring on the system it is used on. As an alternative, export ‘SDKCRAFT_STORE_CREDENTIALS’ with the exported credentials. ### Examples Log in interactively: ```console $ sdkcraft login ``` ## sdkcraft whoami Display login information ### Usage ```console $ sdkcraft whoami ``` ### Description Display information about the currently authenticated user. ### Examples Show current login: ```console $ sdkcraft whoami ``` ## See also Explanation: - [SDK concepts](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-concepts) Reference: - [SDK definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdk-definition.md#ref-sdk-definition) - [SDK internals](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-sdk-internals) Tutorial: - [Craft SDKs with SDKcraft](https://ubuntu.com/workshop/docs//tutorial/part-4-craft-sdks.md#tut-craft-sdks) # sdks.md # SDK internals ## Source directory All files that go into an SDK should be placed in a *source directory* where you’ll run **SDKcraft** to initialize, define, pack and publish the SDK. ## SDK platform A platform describes where an SDK can be built and installed. The components describing a platform are: - The base image: used to build SDKs and initialize workshops. - The CPU architecture: `amd64`, `arm64`, `armhf`, `i386`, `ppc64el`, `riscv64`, or `s390x`. The easiest way to define a platform is to name it after the CPU architecture: ```yaml # ... base: ubuntu@24.04 platforms: amd64: arm64: ``` The above SDK can be built on `amd64` or `arm64` machines and installed in `ubuntu@24.04` workshops with the same architecture. The `base` can also be moved into the platform names: ```yaml # ... platforms: ubuntu@24.04:amd64: ubuntu@24.04:arm64: ``` More complex scenarios can be described using the following attributes: | Key | Value | Description | |-------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `build-on` | array | List of supported CPU architectures
that can build the SDK for the platform.

If the SDK has no `base` or `build-base`,
each entry must be prefixed by a valid base and a colon,
e.g., `ubuntu@22.04:amd64`.
This has no effect on the supported build machines,
because **SDKcraft** performs builds in containers.
The prefix must match `build-for`. | | `build-for` | string | CPU architecture the SDK is expected to run on,
or `all` if the SDK can run on all supported architectures.
SDK authors are responsible for ensuring compatibility.

If the SDK has no `base` or `build-base`,
each entry must be prefixed by a valid base and a colon,
e.g., `ubuntu@24.04:riscv64`.
The prefix must match every entry in `build-on`. | Architecture-independent SDKs require the complex format: ```yaml # ... platforms: all: build-on: [amd64, arm64, riscv64] build-for: all ``` ## SDK parts Parts can be thought of as the building blocks of **Workshop** and **SDKcraft**. Each part in the `sdkcraft.yaml` [definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdk-definition.md#ref-sdk-definition) describes a specific component or piece of the SDK being packaged, providing a way to modularize the package and manage its dependencies. **SDKcraft** is built as a [craft-application](https://github.com/canonical/craft-application/), which affects how `parts` are implemented. However, note that `stage-packages` and `stage-snaps` aren’t enabled yet; instead, rely on the [hooks](#ref-sdk-hooks) to implement custom logic of package and snap installation. For a complete reference of `parts` and their properties, refer to the corresponding Craft Parts [documentation section](https://documentation.ubuntu.com/craft-parts/latest/common/craft-parts/reference/part_properties/). ## SDK plugs and slots Currently, **Workshop** and **SDKcraft** support the following interface plugs: - [Camera](#ref-camera-interface) - [Custom device](#ref-custom-device-interface) - [Desktop](#ref-desktop-interface) - [GPU](#ref-gpu-interface) - [Mount](#ref-mount-interface) - [SSH](#ref-ssh-interface) - [Tunnel](#ref-tunnel-interface) Slots can only be defined for the `mount` interface. ### Camera interface A camera plug in the definition must specify the plug name and the interface: ```yaml # ... plugs: : interface: camera ``` This makes the host’s cameras directly available inside the workshop as video capture devices. ### Custom device interface A custom-device plug in the definition must specify the plug name, the interface, and a `subsystem` attribute: ```yaml # ... plugs: : interface: custom-device subsystem: ``` This makes host devices from the given subsystem directly available inside the workshop. ### Desktop interface A desktop plug in the definition must specify the plug name and the interface: ```yaml # ... plugs: : interface: desktop ``` This makes the host’s Wayland socket directly available inside the workshop. ### GPU interface A GPU plug in the definition must specify the plug name and the interface: ```yaml # ... plugs: gpu: interface: gpu ``` This makes the host’s GPUs directly available inside the workshop via the GPU pass-through mechanism. ### Mount interface A mount plug in the definition must specify the plug name, the interface, and the target directory. The plug can specify permissions and ownership for the target, and whether it is read-only: ```yaml # ... plugs: : interface: mount workshop-target: mode: # optional uid: # optional gid: # optional read-only: # optional ``` This mounts a directory automatically created by **Workshop** on the host to the `workshop-target` directory. The `$SDK` variable can be used to refer to the SDK installation path inside the workshop. The host directory will be created under the path designated by the `$XDG_DATA_HOME` variable. The workshop directory will be created using the given `mode`, `uid`, and `gid`. A mount *slot* in the definition must specify the slot name, the interface, and the *source* directory: ```yaml # ... slots: : interface: mount workshop-source: ``` This exposes the `workshop-source` directory inside the workshop to be mounted to another directory within the workshop. The `$SDK` variable can be used to refer to the SDK installation path inside the workshop. ### SSH interface An SSH plug in the definition must specify the plug name and the interface: ```yaml # ... plugs: ssh-agent: interface: ssh-agent ``` This proxies the host’s SSH keys and configuration inside the workshop via a Unix domain socket. ### Tunnel interface A tunnel plug in the definition must specify the plug name, the interface and optionally an endpoint: ```yaml # ... plugs: : interface: tunnel endpoint: ``` Similarly, a tunnel *slot* in the definition must specify the slot name, the interface and optionally an endpoint: ```yaml # ... slots: : interface: tunnel endpoint: ``` When a tunnel interface plug is connected to a slot, clients can connect to the address of the plug. The connection will be forwarded to the address of the slot. Regular SDKs define the workshop side of the connection, leaving the host system to the `system` SDK. The supported protocols are TCP, UDP and Unix domain sockets. Unix domain sockets are compatible with TCP, but UDP plugs can only connect to UDP slots. TCP and UDP endpoints look like `:/` or `'[]:/'`. **Workshop** doesn’t resolve hostnames, but supports the aliases `localhost`, `ip6-localhost` and `ip6-loopback`. Unix domain socket endpoints are either paths to a socket file or abstract sockets of the form `'@'`. The `$HOME` and `$XDG_RUNTIME_DIR` variables can be used in paths. Attributes can be abbreviated by omitting `tcp` and `localhost`: | Address | Alternatives | |----------------------|---------------------------------------------------------------| | `127.0.0.1:1234/tcp` | `localhost:1234/tcp`, `1234/tcp`, `127.0.0.1:1234`, `1234` | | `0.0.0.0:1234/tcp` | `0.0.0.0:1234` | | `'[::1]:1234/tcp'` | `ip6-localhost:1234/tcp`, `ip6-loopback:1234`, `'[::1]:1234'` | | `127.0.0.1:1234/udp` | `localhost:1234/udp`, `1234/udp` | | `'[::]:1234/udp'` | | | `/run/service.sock` | | | `'@abstract'` | | Port numbers may also be omitted, but only on one side of a connection. For such connections, both sides use the same port. ## SDK hooks **Workshop** supports the following lifecycle hooks, which can be defined when the SDK is built using **SDKcraft**: | Name | When **Workshop** runs it | What it does | |-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `setup-base` | At [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch), [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh):
after unpacking the base image
and starting the workshop,
but before mounting the project directory
and connecting plugs and slots. | Configures system packages and services required by the SDK. | | `setup-project` | At [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch), [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh), [workshop restore](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-restore):
after mounting the project directory
and auto-connecting plugs and slots
but before the workshop is set to *Ready*. | Configures the user environment for the SDK to become operational. | | `save-state` | At [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh), [workshop restore](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-restore):
before destroying the old workshop. | Saves SDK-specific data to the [state directory](#ref-sdk-state).
The hook itself comes from the *old* SDK revision. | | `restore-state` | At [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh), [workshop restore](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-restore):
after running `setup-project` hooks for *all* SDKs. | Restores SDK-specific data from the [state directory](#ref-sdk-state).
The hook itself comes from the *new* SDK revision. | | `check-health` | At [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch):
after running `setup-project` hooks for *all* SDKs.

At [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh), [workshop restore](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-restore):
after running `restore-state` hooks for *all* SDKs. | Sets the state of the SDK
(`okay`, `waiting` or `error`)
using [workshopctl](https://ubuntu.com/workshop/docs//reference/cli/workshopctl.md#ref-workshopctl-cli),
which affects the [status](https://ubuntu.com/workshop/docs//reference/workshop-status.md#ref-workshop-status) of the workshop. | Each hook is defined as a **bash** script of the same name under `hooks/` in the [source directory](#ref-sdk-directory). Inside the workshop, the SDK is mounted at `/var/lib/workshop/sdk//` and hooks are stored in the `sdk/hooks/` subdirectory. Most hooks run as `root` and use that subdirectory as the working directory. The exception is `setup-project`, which runs as the `workshop` user in the `/project/` directory. A hook can signal an error by returning a nonzero exit code; a zero code indicates success. The options `errexit` and `pipefail` are set by default, so most commands which return a nonzero exit code cause the hook to exit with the same code. If `--verbose` is passed to **workshop launch** or **workshop refresh**, the option `xtrace` is also set. #### NOTE The hooks aren’t mentioned in the [SDK definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdk-definition.md#ref-sdk-definition); **SDKcraft** automatically enumerates them when packing the SDK. An SDK’s position in the [workshop definition](https://ubuntu.com/workshop/docs//reference/definition-files/workshop-definition.md#ref-workshop-definition) determines when its hooks execute. SDKs are always processed in the following order: `system`, user-listed SDKs, `sketch`. Each hook waits for the previous one to complete before executing. ## SDK state An SDK can store any data specific to it within the workshop. For this purpose, an environment variable named `$SDK_STATE_DIR` is exposed by **Workshop** at runtime; it resolves to an internal directory in the workshop, which `save-state` and `restore-state` can use to preserve and recover the data respectively. #### NOTE The `$SDK_STATE_DIR` variable is only available to the `save-state` and `restore-state` SDK hooks. It is not accessible to the `workshop` user, the SDK itself, or in the workshop definition. The state directory is a dedicated volume created by **Workshop** at runtime for each SDK in every workshop, and is removed when the workshop stops. The `*-state` hooks can use it to store or retrieve any arbitrary data required by the SDK. ## SDK channels When SDKs are published by their creators and consumed by workshops, different versions and releases are tracked through the use of channels. A channel is a combination of a track, a risk, and an optional branch, e.g., `latest/beta`. Tracks allow multiple published versions of an SDK to exist in parallel; while no specific scheme is enforced, it is desirable to use a semantic version, e.g., `1.2.3`, or the `latest` keyword, which maps to the latest published version and serves as the default. Risks represent a choice of maturity levels for a particular track: - `stable`: indicates that the software can be used in production - `candidate`: for software that’s being tested prior to stable deployment - `beta`: for software that can be used outside of production - `edge`: for unstable software that’s still in active development; nothing is guaranteed Branches are short-lived subdivisions of a channel intended for experimentation, e.g. 1.2.3/edge/issue-56789. After 30 days of no activity, a branch will be closed automatically. #### ATTENTION SDK channels should not be confused with SDK revisions. ## See also Explanation: - [Base image](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-base) - [Camera interface](https://ubuntu.com/workshop/docs//explanation/interfaces/camera-interface.md#exp-camera-interface) - [Custom device interface](https://ubuntu.com/workshop/docs//explanation/interfaces/custom-device-interface.md#exp-custom-device-interface) - [Desktop interface](https://ubuntu.com/workshop/docs//explanation/interfaces/desktop-interface.md#exp-desktop-interface) - [GPU interface](https://ubuntu.com/workshop/docs//explanation/interfaces/gpu-interface.md#exp-gpu-interface) - [Interface concepts](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-concepts) - [Mount interface](https://ubuntu.com/workshop/docs//explanation/interfaces/mount-interface.md#exp-mount-interface) - [SDKs](https://ubuntu.com/workshop/docs//explanation/index.md#exp-sdks) - [SDK state](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-state) - [SSH interface](https://ubuntu.com/workshop/docs//explanation/interfaces/ssh-interface.md#exp-ssh-interface) - [System SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-system-sdk) - [Tunnel interface](https://ubuntu.com/workshop/docs//explanation/interfaces/tunnel-interface.md#exp-tunnel-interface) - [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) # sdks.md # SDKs SDKs are the pre-built, reusable blocks of functionality involved in the definition, design, distribution, and day-to-day operation of a workshop. ## Understanding SDKs At their core, SDKs are bundles of software dependencies distributed through the SDK Store or defined locally; they can pre-package libraries, tools, and configurations or install them directly into a workshop. * [SDK concepts](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md) * [Parts](https://ubuntu.com/workshop/docs//explanation/sdks/parts.md) ## SDK design When you are creating SDKs, it helps to understand how they compare to traditional container approaches and what design patterns lead to maintainable, reusable packages: * [Design best practices](https://ubuntu.com/workshop/docs//explanation/sdks/best-practices.md) * [SDKs versus Dockerfiles](https://ubuntu.com/workshop/docs//explanation/sdks/sdk-vs-dockerfile.md) ## Operations and tooling **Workshop** exposes available SDKs through the **sdk** CLI, lets SDK authors build and publish them with the **sdkcraft** CLI, and ships the in-workshop **workshopctl** helper for SDK hooks to talk back to the daemon: * [sdk (CLI)](https://ubuntu.com/workshop/docs//explanation/sdks/sdk-cli.md) * [sdkcraft (CLI)](https://ubuntu.com/workshop/docs//explanation/sdks/sdkcraft-cli.md) * [workshopctl (CLI)](https://ubuntu.com/workshop/docs//explanation/sdks/workshopctl-cli.md) # security.md # Security policy This is an overview of security considerations for Workshop and SDKcraft. ## Privileges Workshop has a client-server architecture; its CLI, which is the contact surface for the users, is confined as a snap and neither needs nor requires elevated privileges to run. Instead, it uses a RESTful API to communicate with the `workshopd` daemon, which performs all the heavy lifting and does indeed run with elevated privileges. The use of [LXD](https://documentation.ubuntu.com/lxd/latest/) for implementation provides the benefits of a mature container technology. SDKcraft is an instance of [`craft-application`](https://github.com/canonical/craft-application/), built, installed, and run as a snap; it neither needs nor requires elevated privileges to work and securely confines the SDK build process to a container. Packaged SDKs are uploaded to the SDK Store. ## Isolation Users can only access the workshops they have created; these workshops have limited capabilities on the host. To achieve this, LXD is used to add a level of confinement: everything users do ends up in a [nonprivileged container](https://ubuntu.com/server/docs/how-to/containers/lxd-containers/) within a dedicated [project](https://documentation.ubuntu.com/lxd/latest/explanation/projects/), which separates workshops that belong to different users and isolates them from each other and the host system. By design, all SDKs in a workshop can access any data inside it, but have limited capabilities on the host, due to the confinement of the workshop. ## Interfaces In Workshop, the interface mechanism plays a role in maintaining security by controlling access between the workshop’s components and the host system; the implementation is largely similar to `snapd`’s [interface manager](https://snapcraft.io/docs/interface-management/): * Interfaces define and control what resources a workshop can use, ensuring that permissions are explicitly granted and limited in scope. * They are used to explicitly provide access to resources such as files, the GPU, or the SSH agent. * SDKs in a workshop, or the workshop itself, must declare the interfaces and the connections they need. This limits the resources a workshop can access. * Some interfaces, such as mounts, are connected automatically by default; others require manual approval by the user. All connections are subject to built-in validation policies. * The use of interfaces reflects the least privilege principle, allowing publishers and users to request only the necessary permissions, reducing the attack surface. ## Risks Although safeguards are in place, the security of a workshop or an SDK largely depends on how it’s designed. For instance, it is advisable not to store sensitive data within workshops. Instead, use mounts to provide access to data only to the SDKs that require it. Another example is avoiding the connection of sensitive interfaces, such as the SSH agent, unless absolutely necessary. You can use environment variables in Workshop commands for access tokens or the :ref:`SSH interface ` for transparent key-based access to securely handle sensitive data in your SDKs. The SDKs available in a workshop are sourced from the SDK Store and are generally reliable at this stage of development. However, if you are cautious about potential risks, assume from the outset that no SDK is free from security concerns. ## Supported versions Use the latest releases of Workshop and SDKcraft from GitHub; older releases may have known bugs or be incompatible with latest changes. ## Reporting a vulnerability The easiest way to report a security issue is through GitHub, filing a private security report with a description of the issue, affected versions, the steps to reproduce the issue, and, if known, ways of mitigating it. See [Privately reporting a security vulnerability](https://docs.github.com/en/code-security/how-tos/report-and-fix-vulnerabilities/privately-reporting-a-security-vulnerability) for instructions. Our GitHub admins will be notified of the issue and will work with you to determine whether the issue qualifies as a security issue and, if so, in which component. We will then handle figuring out a fix, getting a CVE assigned, and coordinating the release of the fix. The [Ubuntu Security disclosure and embargo policy](https://ubuntu.com/security/disclosure-policy) contains more information about what you can expect when you contact us and what we expect from you. In lower-priority cases that do not affect security, you may report your concerns in [GitHub issues](https://github.com/canonical/workshop/issues). ## Cryptography Transport encryption - CLI ↔ daemon: local Unix domain socket (no TLS required). - Outbound traffic: HTTPS/TLS for simplestreams [image downloads](https://cloud-images.ubuntu.com/releases/) and public LXD remotes via the Go TLS stack (through the LXD client). - Mutual TLS (public LXD remotes): enable by supplying X.509 materials in `/var/lib/workshop/tls` (`server.crt`, `client.crt`, `client.key`, `client.ca`). Internal cryptography - TLS stack: Go’s `crypto/tls` and `crypto/x509` (via the Canonical LXD Go client), using TLS 1.2/1.3 with Go’s secure default cipher suites (ECDHE with AES‑GCM/ChaCha20‑Poly1305) and the system trust store by default. - Randomness: `crypto/rand` for non‑guessable identifiers (e.g., 4‑byte project IDs, 8‑byte layer suffixes); these values are not used for access control. User‑exposed crypto and providers - SSH agent interface: forwards the host’s `ssh-agent` into the workshop via an LXD proxy device, allowing tools inside the container to authenticate without copying private keys. - Algorithms: follow host OpenSSH (commonly Ed25519, ECDSA P‑256/P‑384/P‑521, RSA 2048/3072/4096). - Providers: Go standard library (`crypto/tls`, `crypto/x509`, `crypto/rand`), Canonical LXD Go client (TLS handling), system CA store, and OpenSSH packages from Ubuntu. # ssh-agent.md # SSH interface The SSH interface exposes the user’s SSH agent socket. - Plug attributes: none. - Plug name: must be `ssh-agent`. - Plug owner: any regular SDK; not the system SDK. - Slot: the system SDK provides a single `system:ssh-agent` slot. Other SDKs cannot declare SSH slots. # ssh-interface.md # SSH interface The SSH interface provides access to the host system’s SSH agent from inside the workshop, allowing it to securely use the host’s SSH keys and configuration. By using the interface, the SDK publisher allows the workshop to connect to the host’s SSH agent, which can be useful in various SDK-specific tasks such as cloning private repositories, accessing remote machines, and so on. ## SSH interface plug An essential element here is the SSH interface plug, which is declared in the SDK definition. Its structure includes just the name of the plug and the interface; both must be set to `ssh-agent`. Defining the plug in an SDK allows the workshops using this SDK to connect to the host’s SSH agent, which can be useful in various SDK-specific tasks such as cloning private repositories, accessing remote machines, and so on. ## SSH interface slot To let SDKs in a workshop access the host’s SSH agent, **Workshop** provides an SSH interface slot that multiple SSH interface plugs can access. When the SDK is installed at runtime during launch and refresh operations, **Workshop** checks that the plug targeting the slot passes [validation](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interfaces-validation); if it does, it can be connected. ## Connection The interface isn’t connected automatically at launch and refresh for security reasons. The **workshop connect** and **workshop disconnect** commands can be invoked manually after the workshop has started: ```console $ workshop connect ws/ssh-sdk:ssh-agent $ workshop disconnect ws/ssh-sdk:ssh-agent ``` Establishing a connection means a proxy Unix domain socket has been created and a corresponding `$SSH_AUTH_SOCK` value has been set for the `workshop` user, so the host’s SSH identities and configuration are available inside the workshop. To check if the interface is connected: ```console $ workshop connections --all INTERFACE PLUG SLOT NOTES ... ssh-agent ws/ssh-sdk:ssh-agent ws/system:ssh-agent manual ``` This means the host’s SSH identities and configuration are available inside the workshop: ```console $ workshop shell ws workshop@ws-8584e571$ echo $SSH_AUTH_SOCK /var/lib/workshop/run/ssh-agent.sock workshop@ws-8584e571$ ssh-add -l 4096 SHA256:cb19/bE/6irqhII1KbQqRmo1royWi58qcUD9MEn/9fE user@example.com (RSA) ``` ## See also Explanation: - [Interface concepts](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-concepts) - [Plugs and slots](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-plugs-slots) - [SDK definition](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-definition) - [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) Reference: - [workshop connect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connect) - [workshop connections](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connections) - [workshop disconnect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-disconnect) - [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) - [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh) - [workshop shell](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-shell) # tunnel-interface.md # Tunnel interface The tunnel interface enables workshops to share network services with the host system, and vice versa. It supports connections over TCP, UDP, and Unix domain sockets. SDKs advertise their services using tunnel interface slots. For example, if an SDK installs and advertises a web app, users can access the app from their usual browser after creating a tunnel from the host system to the workshop. SDKs request access to services using tunnel interface plugs. Some services have dedicated interfaces (e.g., the [SSH interface](https://ubuntu.com/workshop/docs//explanation/interfaces/ssh-interface.md#exp-ssh-interface)), which should be used instead. ## Tunnel interface plug Most SDKs declare tunnel interface plugs in their SDK definitions, but the [system SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-system-sdk) has none by default, so system SDK plugs must be declared in the workshop definition. A basic structure would include the name of the plug, the interface (`tunnel`), and, optionally, an address (`endpoint`). Plugs designate addresses that clients can connect to. Regular SDKs are used for clients inside the workshop. The system SDK is used for clients from the host system. ## Tunnel interface slot Most SDKs declare tunnel interface slots in their SDK definitions, but the [system SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-system-sdk) has none by default, so system SDK slots must be declared in the workshop definition. A basic structure would include the name of the slot, the interface (`tunnel`), and, optionally, an address (`endpoint`). Slots designate an address that a service can listen on. Regular SDKs should make this service available inside the workshop. The system SDK relies on the user to run this service on the host. ## Connection The interface is connected automatically at launch or refresh, provided that: - The plug is declared in the system SDK - The slot is declared in a regular SDK - The plug listens on `localhost` or a Unix domain socket - The plug can be matched to the slot by its name, or via a `connections` entry in the [definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition), both subject to **Workshop**’s [validation rules](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interfaces-validation). Otherwise, it isn’t connected automatically, for security reasons. The **workshop connect** and **workshop disconnect** commands can be invoked after the workshop has started: ```console $ workshop connect ws/client-sdk:shared $ workshop disconnect ws/client-sdk:shared $ workshop connect ws/system:app ws/service-sdk:app $ workshop disconnect ws/service-sdk:app ``` Establishing a tunnel connection means that **Workshop** will listen on the plug address, forwarding incoming network connections to the slot address. When a system SDK plug is connected to a regular SDK slot, clients on the host can access services inside the workshop: When a regular SDK plug is connected to a system SDK slot, clients in the workshop can access services on the host: **Workshop** doesn’t support connections within the system SDK or between regular SDKs. In these cases clients can connect to services directly, without the need for a tunnel. To check if a plug or slot is connected: ```console $ workshop connections --all INTERFACE PLUG SLOT NOTES ... tunnel ws/client-sdk:shared ws/system:shared manual tunnel ws/system:app ws/service-sdk:app manual ``` This means that `client-sdk` can access the `shared` service running on the host, and the host can access the `app` service provided by `service-sdk`. ```console $ workshop info dev name: dev base: ubuntu@22.04 project: /home/user/workshop/dev status: ready notes: - sdks: system: tunnels: app: from: 0.0.0.0:8081/tcp to: 127.0.0.1:8080/tcp client-sdk: tracking: latest/stable installed: 2024-03-02 (1) tunnels: shared: from: [::1]:1080/tcp to: 127.0.0.1:18080/tcp service-sdk: tracking: latest/edge installed: 2025-06-07 (2) ``` ## See also Explanation: - [Interface concepts](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-interface-concepts) - [Plugs and slots](https://ubuntu.com/workshop/docs//explanation/interfaces/concepts.md#exp-plugs-slots) - [SDK definition](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-definition) - [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) Reference: - [Tunnel interface](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-tunnel-interface) - [workshop connect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connect) - [workshop connections](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connections) - [workshop disconnect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-disconnect) - [workshop info](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-info) - [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) - [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh) # tunnel.md # Tunnel interface The tunnel interface forwards a network address or Unix domain socket. Both tunnel plugs and tunnel slots take a single attribute: | Key | Value | Description | |------------|---------|-----------------------------------------------------------------------------------------------------------------------------------| | `endpoint` | string | Network address or Unix domain socket that forms one end of the tunnel.
Defaults to `localhost/tcp` for both plugs and slots. | The `endpoint` value follows this grammar: | Field | Format | |----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Endpoint | `
/` for network endpoints;
may be shortened to `
` or `` alone.

`` or `@` for Unix domain sockets. | | Address | `:`; may be shortened to `` or ``. | | Protocol | Either `tcp` or `udp`. Defaults to `tcp`. | | Host | An IPv4 or IPv6 address.
When a port is supplied, IPv6 addresses must be enclosed in square brackets.

Supported aliases: `localhost`, `ip6-localhost`, and `ip6-loopback`.
Defaults to `localhost`. | | Port | A TCP or UDP port number (1-65535).
May be omitted, but only on one side of a connection; both sides then use the same port.

For security, tunnel plugs in the system SDK cannot use privileged ports (1-1023). | | Path | Absolute path to a Unix domain socket.

`$HOME` expands to the user’s home directory
and `$XDG_RUNTIME_DIR` expands to the user runtime directory
(typically `/run/user/1000`).

For security, tunnel plugs in the system SDK cannot listen on sockets outside these two directories. | | String | An abstract socket name. | Endpoints that start with `[` or `@` must be quoted in YAML: ```yaml endpoint: '[::1]:8080/tcp' endpoint: '@abstract.sock' ``` # tutorial.md # Tutorial Anyone new to **Workshop** should start here. The tutorial has four main parts: you will learn to build development environments, customize them, and share the updates with others. The tutorial introduces key tools, concepts, and ways of thinking about workshops and SDKs. Follow the sections of the tutorial in order. * [Part 1: Get started](https://ubuntu.com/workshop/docs//tutorial/part-1-get-started.md) * [Part 2: Work with interfaces](https://ubuntu.com/workshop/docs//tutorial/part-2-work-with-interfaces.md) * [Part 3: Sketch SDKs](https://ubuntu.com/workshop/docs//tutorial/part-3-sketch-sdks.md) * [Part 4: Craft SDKs](https://ubuntu.com/workshop/docs//tutorial/part-4-craft-sdks.md) # use-git.md # How to use workshops with Git Workshops are designed to be used in common development ecosystems, which makes their encounter with Git almost inevitable. Let’s look at how you can integrate workshops into your repo. ## Initialization To start, place the workshop definition in your repository: ```console $ git init original $ cd original/ ``` ```yaml name: dev base: ubuntu@22.04 sdks: - name: go channel: 1.26 ``` Next, launch the workshop and start working on your code: ```console $ workshop launch ``` ```go package main import "fmt" func main() { fmt.Println("hello, Workshop") } ``` Mind that any activity that relies on the workshop’s contents should now occur inside the workshop: ```console $ git add . && git commit -m "initial commit" $ workshop exec dev go build -x main.go ``` However, the resulting artifacts are exposed in the project directory: ```console $ ./main hello, Workshop ``` They stay there even if you remove the workshop: ```console $ workshop remove $ ./main hello, Workshop ``` From here, you can do whatever you like with your repo, because **Workshop** handles [moving projects around](https://ubuntu.com/workshop/docs//how-to/customize-workshops/move-projects.md#how-move-projects) quite well. Don’t forget to add the `.workshop.lock` file to your `.gitignore` file: ```console $ echo ".workshop.lock" >> .gitignore ``` In contrast, the definition and the `.workshop/` directory are *meant* to be stored in a repository; if your `.gitignore` file uses rules such as “ignore everything except these files and directories,” add them to the list of explicitly tracked items. With your dependencies accounted for, restoring your build system after cloning the repo elsewhere is as simple as relaunching the workshop from a new *project directory*. But what if you need to maintain multiple branches that require different versions of the same workshop? A common solution is to clone the repo several times to manually synchronize the copies when needed, but this approach is prone to errors and overhead. Let’s build something better and… #### NOTE If you did remove the workshop at this step of the guide, relaunch it before proceeding further: ```console $ workshop launch ``` ## Use worktrees Let’s add a Git feature that works well with workshops, namely [`git worktree`](https://git-scm.com/docs/git-worktree). One of **Workshop**’s goals is to simplify toggling external dependencies such as frameworks or OS versions. Say you want to investigate a problem that occurs on an older OS version, so you create a new worktree just for that: ```console $ git worktree add ../hotfix $ cd ../hotfix/ ``` Instead of bothering with virtual machines, update the definition to change the base image: ```yaml name: dev base: ubuntu@24.04 sdks: - name: go channel: 1.26 ``` Next, launch the redefined workshop to work on the problem: ```console $ workshop launch $ # Hacking away until the problem is solved $ git commit -m "solve problem with hotfix" $ cd ../original/ $ git merge hotfix ``` As with regular directories, **Workshop** works well with [`git worktree move`](https://git-scm.com/docs/git-worktree#_commands): ```console $ git worktree move ../hotfix/ ../resolved/ $ workshop list --global PROJECT WORKSHOP STATUS NOTES ~/original dev Ready - ~/resolved dev Ready - ``` Similarly, when it comes to clean-up, remove all workshops before running `git worktree remove`: ```console $ workshop remove --project ../resolved/ $ git worktree remove ../resolved/ ``` So using **git worktree** reduces the effort on sync, stash, and pull, while **Workshop** allows you to hot-swap an entire OS or another complex dependency by going from one directory to another. ## See also Explanation: - [Base image](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-base) - [Multi-workshop patterns](https://ubuntu.com/workshop/docs//explanation/workshops/multi-workshop-patterns.md#exp-multi-workshop-patterns) - [Projects](https://ubuntu.com/workshop/docs//explanation/workshops/projects.md#exp-projects) - [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) How-to guides: - [How to move projects around](https://ubuntu.com/workshop/docs//how-to/customize-workshops/move-projects.md#how-move-projects) - [How to use workshops with AI agents](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/use-workshops-with-ai-agents.md#how-use-workshops-with-ai-agents) Reference: - [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) - [workshop remove](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-remove) # use-multiple-workshops.md # How to use multiple workshops in a project A project may require different toolchains for different components, such as a Go backend and a Node.js frontend. Instead of putting everything into a single workshop definition, you can define multiple workshops in the same project directory. Each workshop is an independent environment with its own base image, SDKs, and actions; at the same time, the workshops share a single project directory mounted at `/project/`. ## Set up definitions When a project uses multiple workshops, store their definitions in the `.workshop/` subdirectory instead of a single `workshop.yaml` in the project root. Each file must be named after its workshop, so the `name` field matches the file name (without the `.yaml` extension). Here is a project layout with two workshop definitions and a shared in-project SDK: ```none my-project/ ├── .workshop/ │ ├── frontend.yaml │ ├── backend.yaml │ └── common-tools/ │ └── sdk.yaml ├── web/ └── api/ ``` The `frontend` workshop uses the `node` SDK for the browser-facing part of the project: ```yaml name: frontend base: ubuntu@24.04 sdks: - name: node channel: 24 actions: build: | npm run build ``` The `backend` workshop uses the `go` SDK for the server-side code: ```yaml name: backend base: ubuntu@22.04 sdks: - name: go channel: 1.26 actions: test: | go test ./... ``` Each workshop can use a different base image, a different set of SDKs, and its own actions, all while sharing the project directory. What’s more, you can share in-project SDKs across workshops, as described in a section below. #### NOTE You cannot mix a root-level `workshop.yaml` with files in `.workshop/`. If **Workshop** finds both, it reports an error. ## Launch and manage workshops Launch both workshops at once: ```console $ workshop launch frontend backend ``` When a project has multiple workshops, the workshop name is required in every command; you cannot omit it as you would with a single-workshop project. Check the status of both workshops: ```console $ workshop list WORKSHOP STATUS NOTES frontend Ready - backend Ready - ``` Run an action in a specific workshop: ```console $ workshop run frontend -- build $ workshop run backend -- test ``` Shell into one of the workshops: ```console $ workshop shell backend ``` Execute a one-off command: ```console $ workshop exec frontend -- node --version ``` Stop and start workshops independently: ```console $ workshop stop frontend $ workshop start frontend ``` Or stop both at once: ```console $ workshop stop frontend backend ``` To see the status of workshops in the current project, use the **workshop list** command without arguments: ```console $ workshop list WORKSHOP STATUS NOTES frontend Ready - backend Ready - ``` To see the status of workshops across all projects on the system, use the `--global` flag: ```console $ workshop list --global PROJECT WORKSHOP STATUS NOTES /home/user/my-project frontend Ready - /home/user/my-project backend Ready - /home/user/other-project dev Ready - ``` When you no longer need the workshops, remove them: ```console $ workshop remove frontend backend ``` ## Share in-project tools If multiple workshops need the same custom tooling, define an in-project SDK rather than duplicating hooks or configuration. In-project SDKs are stored in subdirectories of `.workshop/` and referenced with the `project-` prefix. Both workshops can then include it: ```yaml name: frontend base: ubuntu@24.04 sdks: - name: node channel: 24 - name: project-common-tools ``` ```yaml name: backend base: ubuntu@22.04 sdks: - name: go channel: 1.26 - name: project-common-tools ``` After adding the SDK references, refresh the workshops to pick up the change: ```console $ workshop refresh frontend backend ``` ## Cross-workshop networking You cannot connect a plug in one workshop to a slot in another; **workshop connect** rejects such attempts. However, all workshops on the same machine share a common host, and the tunnel interface can bridge through it. The idea is to compose two independent tunnels: one that exposes a service from the backend workshop to the host, and another that lets the frontend workshop reach that host port. This is different from a regular intraworkshop connection, where a single tunnel links a plug to a slot inside the same workshop. Here, the host sits in the middle, and each workshop configures its own half of the bridge. The backend workshop exposes its API on the host by pairing a `system` plug with a regular SDK slot: ```yaml name: backend base: ubuntu@22.04 sdks: - name: go channel: 1.26 slots: api: interface: tunnel endpoint: localhost:8080 # service inside the workshop - name: system plugs: api: interface: tunnel endpoint: localhost:8080 # port on the host ``` The frontend workshop reaches the host port by pairing a regular SDK plug with a `system` slot: ```yaml name: frontend base: ubuntu@24.04 sdks: - name: node channel: 24 plugs: api: interface: tunnel endpoint: localhost:8080 # where the client connects - name: system slots: api: interface: tunnel endpoint: localhost:8080 # host-side port (bridged from backend) ``` Launch both workshops. The backend tunnel auto-connects because its plug is on the `system` SDK with a matching name, but the frontend tunnel requires a manual step: ```console $ workshop launch frontend backend $ workshop connect frontend/node:api ``` Verify the connection: ```console $ workshop connections frontend INTERFACE PLUG SLOT NOTES tunnel frontend/node:api frontend/system:api manual ``` After this, any service listening on port 8080 inside the backend workshop is reachable at `localhost:8080` from within the frontend workshop. #### NOTE The host port must be free before launching the backend workshop, or the tunnel will fail to activate. If you need several cross-workshop tunnels, use a different port for each. See [How to forward ports with tunneling](https://ubuntu.com/workshop/docs//how-to/customize-workshops/forward-ports.md#how-forward-ports) for tunnel basics and troubleshooting. ## See also Explanation: - [In-project SDKs](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-in-project-sdk) - [Multi-workshop patterns](https://ubuntu.com/workshop/docs//explanation/workshops/multi-workshop-patterns.md#exp-multi-workshop-patterns) - [Projects](https://ubuntu.com/workshop/docs//explanation/workshops/projects.md#exp-projects) - [Tunnel interface](https://ubuntu.com/workshop/docs//explanation/interfaces/tunnel-interface.md#exp-tunnel-interface) - [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) How-to guides: - [How to add actions to your workshop](https://ubuntu.com/workshop/docs//how-to/customize-workshops/add-actions.md#how-add-actions) - [How to forward ports with tunneling](https://ubuntu.com/workshop/docs//how-to/customize-workshops/forward-ports.md#how-forward-ports) - [How to use workshops with Git](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/use-git.md#how-git-workshops) - [How to move projects around](https://ubuntu.com/workshop/docs//how-to/customize-workshops/move-projects.md#how-move-projects) Reference: - [workshop connect](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connect) - [workshop connections](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-connections) - [Workshop definition](https://ubuntu.com/workshop/docs//reference/definition-files/workshop-definition.md#ref-workshop-definition) - [workshop exec](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-exec) - [workshop launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch) - [workshop list](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-list) - [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh) - [workshop remove](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-remove) - [workshop run](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-run) - [workshop shell](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-shell) - [workshop start](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-start) - [workshop stop](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-stop) # use-workshops-with-ai-agents.md # How to use workshops with AI agents **Workshop** enables multiple tools and utilities, brought together as SDKs, to work in sandboxed environments over a shared project repository, thus addressing security and privacy concerns while enabling a degree of creativity in your workflows. At the same time, **Workshop** treats AI coding agents as another class of tool that benefits from the container boundary and a shared sandbox. Today, developer teams routinely delegate multistep tasks to agents, run different models for planning and implementation, and coordinate fleets of agents across branches or repos. In practice, each agent and model comes with its own execution model, sandbox mode, and approval policies, so **Workshop** as a consistent container boundary is a safer baseline than relying on every tool to self-sandbox correctly. Running heterogeneous AI coding SDKs in separate Git worktrees over a shared codebase is a best practice recommended by [Anthropic](https://code.claude.com/docs/en/common-workflows#run-parallel-sessions-with-worktrees), [OpenAI](https://developers.openai.com/codex/app/worktrees), and [Cursor](https://cursor.com/blog/agent-best-practices#native-worktree-support). Two scenarios below build a minimal Flask app with a few HTTP routes. Each compares agent outputs and synthesizes the optimal approach in different ways: - Scenario 1: Parallel exploration. Run **claude-code** and **copilot** on the same task, then compare implementations and synthesize their best elements. - Scenario 2: Role-based coding. Assign distinct roles to different agents: **claude-code** as the architect, creating planning documents, **copilot** as the coder, implementing the design. #### NOTE For details of how agents can operate **Workshop**, rather than the other way around, see [Workshop and AI agents](https://ubuntu.com/workshop/docs//reference/ai-agents.md#ref-ai-agents). ## Prerequisites Working with AI agents in workshops builds on [How to use workshops with Git](https://ubuntu.com/workshop/docs//how-to/develop-with-workshops/use-git.md#how-git-workshops) and [How to add actions to your workshop](https://ubuntu.com/workshop/docs//how-to/customize-workshops/add-actions.md#how-add-actions), particularly the use of Git worktrees with workshops. Worktrees help isolate and track different agents’ runs and outcomes while sharing the same project directory and the workshops in it. Note that the **claude-code** and **copilot** SDKs will prompt for login credentials on their first run; you should have a browser window open with the respective account signed in. Alternatively, the agents support token-based API authentication via environment variables, which allows you to skip the login steps below; refer to their respective documentation and runtime help for details. ## Agent prompts Start with a fresh directory and initialize a Git repository: ```console $ mkdir flask-project && cd flask-project $ git init ``` This is the `main` branch, where you would normally store your codebase to be shared across worktrees. We don’t have anything yet, so create a `prompts/` directory at the repository root to store our agent prompts instead: ```console $ mkdir prompts ``` Create the shared prompt for initial parallel exploration that builds a minimal Flask app: ### prompts/flask-shared.txt ```default You are building a minimal Flask app in this repository. Goals: - Provide a small app with 3 routes: /, /health, and /echo. - Keep everything in a single file named app.py. - Use only Flask as a dependency. - Add requirements.txt with a pinned Flask version. - Add a short README.md with run instructions and example curl commands. Route behavior: - / returns a short welcome message. - /health returns JSON with status=ok. - /echo accepts GET and POST. For GET, render a minimal HTML form. For POST, echo the submitted text back as JSON. Constraints: - Do not add extra files or frameworks. - Do not include database or external services. - Keep the code simple and readable. Output: - Create or update app.py, requirements.txt, and README.md accordingly. ``` Add the synthesis prompt: ### prompts/flask-synthesis.txt ```default You are synthesizing two alternative Flask implementations. Context: - One implementation is in /project/claude. - Another implementation is in /project/copilot. - You are working in the current worktree. Task: - Compare both implementations. - Ask any clarification questions you need. - Produce a single minimal Flask app that meets the original requirements. Requirements summary: - app.py only, with routes /, /health, /echo. - requirements.txt with Flask pinned. - README.md with run instructions and curl examples. - Keep dependencies minimal and behavior consistent. Deliverables: - A final, working version in the current worktree. - Keep the implementation simple and avoid extra features. ``` Create the architect prompt for Scenario 2 (relies on the shared prompt above): ### prompts/flask-architect.txt ```default You are the architect for a minimal Flask app. Task: - Produce a short plan for implementing a simple Flask service. - Specify the file list and the route behavior. - Keep the scope minimal and focused. Requirements: - Use app.py as the only code file. - Provide three routes: /, /health, /echo. - Provide requirements.txt with Flask pinned. - Provide README.md with run instructions and curl examples. Output: - A concise plan and file list. ``` Follow up with the coder prompt (also relies on the shared prompt above): ### prompts/flask-coder.txt ```default Implement the plan from prompts/flask-architect.txt. Requirements: - app.py only, with routes /, /health, /echo. - / returns a welcome message. - /health returns JSON with status=ok. - /echo renders a minimal HTML form on GET and echoes submitted text on POST. - requirements.txt pins Flask. - README.md includes run instructions and curl examples. Constraints: - Do not add extra files or dependencies. - Keep the implementation minimal and readable. Deliverables: - app.py, requirements.txt, README.md in this worktree. ``` Commit the prompts so that they are available across all worktrees for reuse and customization: ```console $ git add prompts && git commit -m "add prompts" ``` ## Workshop definition We will be using a single workshop definition that adds two different agents as SDKs. You can choose a different approach for manageability. Create a workshop definition file in the project root: ```yaml name: agent-dev base: ubuntu@24.04 sdks: - name: claude-code - name: copilot actions: claude-auto: | claude --model $CLAUDE_MODEL --dangerously-skip-permissions --print "$@" claude: | claude --model $CLAUDE_MODEL --dangerously-skip-permissions "$@" copilot-auto: | copilot --model $COPILOT_MODEL --yolo --silent --prompt "$@" copilot: | copilot --model $COPILOT_MODEL --yolo --interactive "$@" ``` The definition adds the two SDKs, keeping them isolated from your host system while they work against your shared codebase. The `actions` section defines shell commands that encapsulate the complexity of running different agents with their specific options and idioms; note that all safeguards are disabled because the workshop acts as a shared sandbox, so there’s no need to manage per-agent policies or have these agents installed on your host. Even with `--yolo` or `--dangerously-skip-permissions`, any changes done by an agent remain contained inside the workshop. Save the definition and add it to `.gitignore`, along with the `.workshop.lock` file: ```console $ cat >> .gitignore << EOF .workshop.lock .workshop.yaml EOF ``` This ensures there’s only one workshop definition across all worktrees; otherwise, the definitions and lock files in different worktrees would interfere with each other. #### NOTE If you use multiple workshops in your project, add the `.workshop/` directory and `*.lock` instead. Also, you may have valid reasons to commit these (team reuse, versioning); make sure you understand the implications. ## Scenario 1: Parallel runs This scenario runs two different AI agents on the same prompt, then compares the two implementations and synthesizes an optimal approach. Each agent runs over its own Git worktree, which allows them to operate in parallel without interfering with each other. However, the agents can and should share the workshop, so launch it: ```console $ workshop launch ``` #### NOTE Enable autocompletion for **Workshop** to speed up command entry and avoid mistakes in subcommands, plugs, and slots. ### First worktree Create a worktree for the first agent, `claude-code`: ```console $ git worktree add claude ``` Next, run the first agent in noninteractive mode with the shared prompt, specifying the model to use via an environment variable and the worktree as the working directory: ```console $ workshop exec -- claude login # First time only $ workshop run --env CLAUDE_MODEL=sonnet -w /project/claude -- \ claude-auto "Follow the instructions in ./prompts/flask-shared.txt" ``` The agent generates a (presumably) complete Flask app. In a regular development workflow, you would iterate over the shared codebase here, adding a feature or fixing a bug. You don’t need to wait for the run to finish; proceed to the next step, opening a new terminal tab. ### Second worktree Create a worktree for the second agent: ```console $ git worktree add copilot ``` Run the second agent in noninteractive mode with the same prompt, specifying the model to use via an environment variable and the new worktree as the working directory: ```console $ workshop exec -- copilot # First time only: login $ workshop run --env COPILOT_MODEL=gpt-5.1-codex -w /project/copilot -- \ copilot-auto "Follow the instructions in ./prompts/flask-shared.txt" ``` In a regular development workflow, you would expect this to produce an alternative solution to the same problem. ### Synthesis Create a third worktree where a third run will compare and join both implementations: ```console $ git worktree add synthesis ``` Run the `claude-code` agent in interactive mode with a smarter model and the architect prompt, supplying the worktree as the working directory: ```console $ workshop run --env CLAUDE_MODEL=opus -w /project/synthesis -- \ claude "Follow the instructions in ./prompts/flask-synthesis.txt" ``` The synthesis agent walks through both implementations interactively, asking questions: ```text Q: Implementation A keeps everything in :file:`app.py`, while Implementation B splits out helpers. Which layout do you prefer? A: option B ``` After gathering your preferences, the agent creates the final synthesized implementation. In a regular development workflow, you would cherry-pick the best design choices between the two alternatives, eventually merging the result into `main`. ## Scenario 2: Role-based coding This scenario demonstrates a different workflow where agents take on distinct roles: one as an architect creating planning documents, another as a coder implementing the design. ### Design worktree Start fresh from the project root, creating the design worktree: ```console $ git worktree add design ``` Run the `claude-code` agent in interactive mode with a smarter model and the synthesis prompt, supplying the worktree as the working directory: ```console $ workshop run --env CLAUDE_MODEL=opus -w /project/design -- \ claude "Follow the instructions in ./prompts/flask-architect.txt" ``` The agent eventually creates several planning documents in the worktree. In a regular development workflow, you would iterate over the design, testing some rapid prototypes to validate the approach and refining the plans until they are ready. ### Implementation worktree Next, create the implementation worktree: ```console $ git worktree add implementation ``` Run the implementation agent in noninteractive mode with the coder prompt, supplying the worktree as the working directory: ```console $ workshop run --env COPILOT_MODEL=gemini-3-pro-preview -w /project/implementation -- \ copilot-auto "Follow the instructions in ./prompts/flask-coder.txt" ``` The coder agent reads the planning documents, tracked in a separate branch, and (presumably) implements the design in one go. In a regular development workflow, you would use the coder to implement new features and fix bugs, often switching between the design and implementation worktrees. ## Conclusion The scenarios above demonstrate **Workshop** usage with one example. For your own projects, adapt the scenarios to your orchestration needs. Treat them as a starting point, not a template that must fit every use case. **Workshop** provides a versatile environment for development workflows that involve multiple complex tools such as AI agents. The scenarios above demonstrate two popular patterns, but real-world use cases could also include: - Hybrid approach with a single architect and multiple parallel coders - Evals and benchmarks for side-by-side comparison - Multi-layered role orchestration across many branches - Additional personas, such as analyst, tester, or product owner, each with their own branch and agentic stack; extra capabilities such as skills or subagents can be used, too By running agents in isolated worktrees with a shared workshop sandbox, you gain the benefits of security and privacy while having the flexibility to mix and match different toolchains and workflows. ## See also Explanation: - [Multi-workshop patterns](https://ubuntu.com/workshop/docs//explanation/workshops/multi-workshop-patterns.md#exp-multi-workshop-patterns) - [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) - [Actions](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition-actions) Reference: - [Workshop and AI agents](https://ubuntu.com/workshop/docs//reference/ai-agents.md#ref-ai-agents) - [workshop actions](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-actions) - [workshop exec](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-exec) - [workshop run](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-run) # v0.9.0.md # Workshop v0.9.0 release notes ## 26 May 2026 These release notes cover new features and changes in Workshop v0.9.0. ## Requirements and compatibility Workshop relies on Snap and LXD: - See the [Tutorial](https://ubuntu.com/workshop/docs/tutorial/) for setup instructions. - Refer to the [Contribution Guide](https://ubuntu.com/workshop/docs/contributing/) for development prerequisites. ## What’s new in Workshop v0.9.0 Workshop v0.9.0 is the first public release on the 0.9 track. The project is now licensed under the GNU General Public License v3. This release also introduces the `workshop init` command, a startup firewall check, GPU passthrough via the LXD CDI extension, a redesigned snapshot system, Ubuntu 26.04 support, and arm64 snap builds. ### Public release under GPL v3 Workshop is now publicly released and licensed under the GNU General Public License v3. ### Workshop init command A new `workshop init` command scaffolds a workshop definition in the current directory, replacing the previous need to copy and adapt boilerplate from another project. ### Startup firewall check On startup, Workshop now inspects the host nftables rules that apply to the LXD bridge. If Docker or UFW defaults block forwarding, Workshop logs a message and records a state warning. Workshops still start, so the operator can act before networking issues surface as opaque failures. ### GPU passthrough via LXD CDI The `gpu` interface now uses the Container Device Interface (CDI) extension exposed by recent LXD releases. This aligns Workshop’s GPU passthrough with the mechanism used by the rest of the LXD ecosystem. ### Snapshot system overhaul Snapshots are now workshop-agnostic. A snapshot is keyed by its base image and SDK content rather than by the workshop that produced it, so a single snapshot can seed any workshop with a matching base and SDK set. ### Ubuntu 26.04 support Workshops can now use `ubuntu@26.04` as a base. ### arm64 snap builds Workshop is now published to the snap edge channel for `arm64` in addition to `amd64`. --- **Full Changelog**: [https://github.com/canonical/workshop/compare/v0.1.30…v0.9.0](https://github.com/canonical/workshop/compare/v0.1.30...v0.9.0) # v0.9.1.md # Workshop and SDKcraft 0.9.1 release notes ## 8 June 2026 These release notes cover new features and changes in Workshop and SDKcraft 0.9.1. ## Requirements and compatibility Workshop relies on Snap and LXD: - See the [Tutorial](https://ubuntu.com/workshop/docs/tutorial/) for setup instructions. - Refer to the [Contribution Guide](https://ubuntu.com/workshop/docs/contributing/) for development prerequisites. ## What’s new in 0.9.1 Workshop and SDKcraft 0.9.1 add a `custom-device` interface, the new `sdkcraft revisions` command, and `ubuntu@26.04` base support, alongside several fixes and clearer command output. ### Workshop **Repository:** [https://github.com/canonical/workshop](https://github.com/canonical/workshop) - Added a new `custom-device` interface: ```yaml plugs: : interface: custom-device subsystem: # e.g. accel or input ``` - Fixed unused SDK snapshots that remained after a workshop removal ([issue #799](https://github.com/canonical/workshop/issues/799)). - Fixed `workshop exec` change and task summaries so they show the user-requested command instead of Workshop’s internal `sudo` execution wrapper. - Added retry logic for failed pre-handshake requests to the SDK Store. - Simplified the error messages for `refresh`. - Added support for the `website` field in the `sdk info` output for SDKs. ### SDKcraft **Repository:** [https://github.com/canonical/sdkcraft](https://github.com/canonical/sdkcraft) - Added `sdkcraft revisions`, a new command to list the SDK revisions available on the store. - Added support for the `ubuntu@26.04` base. ### Reference SDKs **Repository:** [https://github.com/canonical/reference-sdks](https://github.com/canonical/reference-sdks) - Added `ubuntu@26.04` base support to the reference SDKs (publishing is in progress and available soon). - Added tests to the agentic and toolchain SDKs, with more automated end-to-end test coverage planned. --- **Full Changelog**: - Workshop: [https://github.com/canonical/workshop/compare/v0.9.0…v0.9.1](https://github.com/canonical/workshop/compare/v0.9.0...v0.9.1) - SDKcraft: [https://github.com/canonical/sdkcraft/compare/0.1.14…0.9.1](https://github.com/canonical/sdkcraft/compare/0.1.14...0.9.1) # workshop-cli.md # workshop (CLI) **Workshop** includes an eponymous command-line utility, **workshop**; it is the daily go-to instrument for regular users, with a set of commands that govern the entire lifecycle of a [workshop](https://ubuntu.com/workshop/docs//explanation/index.md#exp-workshop). There are several categories of commands that vary by their purpose: | Actions | Commands | What they do | |------------------------|-------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------| | Create, update, delete | **launch**,
**refresh**,
**remove**,
**restore**,
**start**,
**stop** | Control a workshop’s existence and runtime state,
from first launch to refresh, restore, and removal. | | Customize | **sketch-sdk**,
**sketches** | Augment a workshop with project-specific customizations
through sketch SDKs. | | Enumerate | **info**,
**list** | List the workshops in a project and inspect their current details. | | Track changes | **changes**,
**tasks** | Review recent changes to the workshops in a project
and the tasks that make up each change. | | Manage connections | **connect**,
**connections**,
**disconnect**,
**remount** | Wire interface plugs and slots between SDKs,
list existing connections, and remount their sources. | | Run shell commands | **exec**,
**shell** | Run an ad-hoc command in a workshop
or open an interactive shell inside it. | | Run named actions | **actions**,
**run** | List and invoke the named actions
defined in a workshop’s `actions:` section. | | Manage warnings | **okay**,
**warnings** | List warnings raised by the daemon and acknowledge them. | For an end-to-end example of putting these commands to use, refer to the [tutorial](https://ubuntu.com/workshop/docs//tutorial/index.md#tut-index). #### NOTE The utility talks to the **Workshop** daemon, **workshopd**, via a REST API, so alternatives are possible and, in fact, encouraged. ## See also Reference: - [Command-line interfaces](https://ubuntu.com/workshop/docs//reference/index.md#ref-cli) Tutorial: - [Get started with workshops](https://ubuntu.com/workshop/docs//tutorial/part-1-get-started.md#tut-get-started) # workshop-definition.md # Workshop definition A *workshop definition* is the YAML file that **Workshop** reads to launch and refresh a workshop. It names the base image, lists the SDKs to install, declares any extra plugs, slots, or connections, and records reusable shell actions. The file is authored by the workshop’s user. ## Filename and location A project may store a single workshop definition at its root, or several under `.workshop/`. - A single workshop: `workshop.yaml` or `.workshop.yaml` in the project directory. - Multiple workshops: `.workshop/.yaml`, one file per workshop. The `` part of the filename must equal the workshop’s `name` field. - A workshop name must start with a lowercase letter and may contain lowercase letters, digits, and hyphens between them. Up to 40 characters. ## Top-level fields | Key | Value | Description | |-------------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `name` (required) | string | Workshop identifier. Subject to the naming rules above.
Must match the filename when the definition is under `.workshop/`. | | `base` (required) | string | Base operating system image.
One of `ubuntu@20.04`, `ubuntu@22.04`, `ubuntu@24.04`,
or `ubuntu@26.04`.

SDKs that declare a `base` must use the same value;
SDKs without a `base` are accepted on any workshop. | | `sdks` | array | Ordered list of SDK entries.
Each entry references an existing SDK and configures it for the workshop.
The system SDK is installed first implicitly and is not required here.
See [SDK entry](#ref-workshop-definition-sdk-entry). | | `connections` | array | Explicit connections between plugs and slots,
applied on top of **Workshop**’s auto-connect logic.
See [Connection entry](#ref-workshop-definition-connection-entry). | | `actions` | object | Named shell scripts available via **workshop run**.
See [Action entry](#ref-workshop-definition-action-entry). | ## Nested structures ### SDK entry Each item in `sdks` is an object with these fields: | Key | Value | Description | |-------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `name` (required) | string | SDK identifier. The underlying name must contain
at least one lowercase letter and may consist of
lowercase letters, digits, and hyphens between them.
`agent` is reserved.

Use a prefix to select the source:

- no prefix: an SDK from the SDK Store (default).
- `try-`:
a locally tried SDK in the [try area](https://ubuntu.com/workshop/docs//reference/cli/sdkcraft.md#ref-sdkcraft-try).
- `project-`:
an in-project SDK defined under `.workshop//`.
- `system`:
the built-in system SDK; listing it explicitly is rarely needed.

The fully prefixed name is at most 40 characters without a prefix,
44 with `try-`, and 48 with `project-`. | | `channel` | string | Store channel from which to retrieve the SDK
at [launch](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-launch)
and [refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-refresh).
Uses the [snap channel format](https://snapcraft.io/docs/channels/):
`//`,
with all three parts optional except that at least one must be present.

Default: `latest/stable`.
Has no effect for `try-`, `project-`, and `system` SDKs,
but must still be well formed.

#### NOTE
Quote channel values in YAML when they look numeric
(for example, `channel: "1.26"`)
to avoid type coercion. | | `plugs` | object | Plug bindings or additional plug definitions grafted onto the SDK
by this workshop.
See [Plug or slot entry (under an SDK)](#ref-workshop-definition-plug-slot)
and [Interfaces](#ref-workshop-definition-interfaces). | | `slots` | object | Additional slot definitions grafted onto the SDK by this workshop.
Each entry specifies the `interface`
and any interface-specific attributes.
See [Interfaces](#ref-workshop-definition-interfaces). | ### Plug or slot entry (under an SDK) Each plug under an SDK is either an inline plug definition or a binding to another plug. Slots under an SDK are always inline slot definitions; slots cannot be bound. | Key | Value | Description | |-------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `interface` | string | Required for an inline plug definition; identifies the interface
(for example, `mount`, `tunnel`).
See [Interfaces](#ref-workshop-definition-interfaces)
for the attributes each interface accepts. | | `bind` | string | Reference to a target plug, in the form `:`.
The `` part must name a non-system SDK,
since bound plugs cannot target the system SDK.

A bound plug must not carry any other attributes,
cannot belong to the system SDK, cannot chain
(bind to a plug that is itself bound),
and cannot also appear in `connections`. | | any interface attribute | varies | Inline plug definitions accept the attributes
documented under [Interfaces](#ref-workshop-definition-interfaces). | ### Connection entry Each item in `connections` links a plug to a slot of the same interface: | Key | Value | Description | |-------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `plug` (required) | string | Plug reference, in the form `:`.
The `` part may be empty (for example, `:ssh-agent`)
to refer to the system SDK.
The referenced SDK must appear in `sdks` or be implicit
(`system`, `sketch`). | | `slot` (required) | string | Slot reference, in the form `:`.
Same rules as `plug`. | A plug that has a `bind` set under its SDK entry cannot also be listed in `connections`. ### Action entry Each entry in `actions` maps an action name to a shell script body: | Key | Value | Description | |-------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | action name | string | Must start with a lowercase letter and may contain lowercase letters,
digits, and hyphens between them. | | action body | string | A **bash** script.
**Workshop** sets `errexit` and `pipefail` before running it.
Arguments passed after **workshop run ** are available
as the standard positional parameters `"$@"`, `"$1"`,
and so on. | Actions are interpreted lazily: edits to `actions` are available immediately, without **workshop refresh**. ## Interfaces The attributes accepted by inline plug and slot definitions depend on the interface. These same attributes appear in SDK definitions ([SDK definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdk-definition.md#ref-sdk-definition) and [SDKcraft project definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdkcraft-definition.md#ref-sdkcraft-definition)); a workshop may graft additional plugs and slots that follow them. ### Camera interface The camera interface exposes a host camera device. - Plug attributes: none. - Plug name: must be `camera`. - Plug owner: any regular SDK; not the system SDK. - Slot: the system SDK provides a single `system:camera` slot. Other SDKs cannot declare camera slots. ### Custom device interface The custom device interface exposes host devices that belong to a Linux kernel subsystem. A custom device plug is described by this attribute: | Key | Value | Description | |------------------------|---------|-----------------------------------------------------------------------------------------------------| | `subsystem` (required) | string | The Linux kernel subsystem of the host devices to expose,
for example `input`, `tty`, or `usb`. | Plug owner: any regular SDK; not the system SDK. Slot: the system SDK provides a single `system:custom-device` slot. Other SDKs cannot declare custom device slots. ### Desktop interface The desktop interface exposes the host display server. - Plug attributes: none. - Plug name: must be `desktop`. - Plug owner: any regular SDK; not the system SDK. - Slot: the system SDK provides a single `system:desktop` slot. Other SDKs cannot declare desktop slots. ### GPU interface The GPU interface exposes host GPU devices. - Plug attributes: none. - Plug name: must be `gpu`. - Plug owner: any regular SDK; not the system SDK. - Slot: the system SDK provides a single `system:gpu` slot. Other SDKs cannot declare GPU slots. ### Mount interface The mount interface exposes a directory between a slot owner and a plug owner. A mount plug is described by these attributes: | Key | Value | Description | |------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `workshop-target` (required) | string | Path inside the workshop used as the plug’s target directory.
Must be an absolute path;
`$SDK` expands to the SDK’s installation path in the workshop. | | `mode` | integer | File permissions, in octal, applied when creating `workshop-target`
and any missing parent directories.
Defaults to `0o775` for regular users.
When `uid` is zero, defaults to `0o755`. | | `uid` | integer | User ID applied when creating `workshop-target`
and any missing parent directories.
Defaults to `1000` when `workshop-target` is under
`/home/workshop/`, `/project/`, or `/run/user/1000/`.
Defaults to `0` otherwise. | | `gid` | integer | Group ID applied when creating `workshop-target`
and any missing parent directories.
Defaults to `1000` or `0`
by the same path rule as `uid`,
even when `uid` is set explicitly. | | `read-only` | Boolean | Whether the target directory should be read-only.
Defaults to `false`. | Plug owner: any regular SDK; not the system SDK. The system SDK provides one mount slot, `system:mount`, with a dynamic `host-source` attribute that can be configured only at [remount](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-remount). It is the only mount slot whose source is on the host filesystem. A mount slot on a regular SDK is described by this attribute: | Key | Value | Description | |------------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| | `workshop-source` (required) | string | Path inside the workshop used as the slot’s source directory.
Must be an absolute path;
`$SDK` expands to the SDK’s installation path in the workshop. | ### SSH interface The SSH interface exposes the user’s SSH agent socket. - Plug attributes: none. - Plug name: must be `ssh-agent`. - Plug owner: any regular SDK; not the system SDK. - Slot: the system SDK provides a single `system:ssh-agent` slot. Other SDKs cannot declare SSH slots. ### Tunnel interface The tunnel interface forwards a network address or Unix domain socket. Both tunnel plugs and tunnel slots take a single attribute: | Key | Value | Description | |------------|---------|-----------------------------------------------------------------------------------------------------------------------------------| | `endpoint` | string | Network address or Unix domain socket that forms one end of the tunnel.
Defaults to `localhost/tcp` for both plugs and slots. | The `endpoint` value follows this grammar: | Field | Format | |----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Endpoint | `
/` for network endpoints;
may be shortened to `
` or `` alone.

`` or `@` for Unix domain sockets. | | Address | `:`; may be shortened to `` or ``. | | Protocol | Either `tcp` or `udp`. Defaults to `tcp`. | | Host | An IPv4 or IPv6 address.
When a port is supplied, IPv6 addresses must be enclosed in square brackets.

Supported aliases: `localhost`, `ip6-localhost`, and `ip6-loopback`.
Defaults to `localhost`. | | Port | A TCP or UDP port number (1-65535).
May be omitted, but only on one side of a connection; both sides then use the same port.

For security, tunnel plugs in the system SDK cannot use privileged ports (1-1023). | | Path | Absolute path to a Unix domain socket.

`$HOME` expands to the user’s home directory
and `$XDG_RUNTIME_DIR` expands to the user runtime directory
(typically `/run/user/1000`).

For security, tunnel plugs in the system SDK cannot listen on sockets outside these two directories. | | String | An abstract socket name. | Endpoints that start with `[` or `@` must be quoted in YAML: ```yaml endpoint: '[::1]:8080/tcp' endpoint: '@abstract.sock' ``` ## JSON Schema The following JSON Schema describes the structure above: ### Workshop definition schema ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://canonical.com/workshop.yaml", "title": "Workshop", "description": "Workshop definition.", "type": "object", "properties": { "name": { "type": "string", "description": "Workshop name. Must start with a lowercase letter and may contain lowercase letters, digits, and hyphens between them. Up to 40 characters. Must match the definition file basename if the workshop is stored under .workshop/.", "pattern": "^[a-z](?:-?[a-z0-9])*$", "maxLength": 40, "errorMessage": "A workshop's name must start with a lowercase letter and can only include digits, lowercase letters, and hyphens joining them." }, "base": { "type": "string", "description": "Base operating system image for the workshop. Must be one of the supported Ubuntu releases. SDKs with a declared base must match the workshop base; SDKs without a base are accepted on any workshop.", "enum": [ "ubuntu@20.04", "ubuntu@22.04", "ubuntu@24.04", "ubuntu@26.04" ], "errorMessage": "The base must be one of the supported values: ubuntu@20.04, ubuntu@22.04, ubuntu@24.04, ubuntu@26.04." }, "sdks": { "type": "array", "description": "Ordered list of SDKs to install on top of the base. Each entry references an existing SDK; names must be unique within the list. The system SDK is installed first implicitly and need not be listed.", "uniqueItems": true, "items": { "type": "object", "properties": { "name": { "type": "string", "description": "SDK name. Prefix with try- for a locally tried SDK, project- for an in-project SDK, or use system for the built-in system SDK; an unprefixed name resolves to an SDK from the Store. The underlying name must contain at least one lowercase letter, may consist of lowercase letters, digits, and hyphens between them, and cannot be agent. Without a prefix the name is at most 40 characters; with try- at most 44; with project- at most 48.", "if": { "pattern": "^try-" }, "then": { "maxLength": 44, "pattern": "^(?!(?:try-|project-)?agent$)try-(?!try-|project-)(?:[a-z0-9]-?)*[a-z](?:-?[a-z0-9])*$" }, "else": { "if": { "pattern": "^project-" }, "then": { "maxLength": 48, "pattern": "^(?!(?:try-|project-)?agent$)project-(?!try-|project-)(?:[a-z0-9]-?)*[a-z](?:-?[a-z0-9])*$" }, "else": { "maxLength": 40, "pattern": "^(?!(?:try-|project-)?agent$)(?!try-|project-)(?:[a-z0-9]-?)*[a-z](?:-?[a-z0-9])*$" } }, "errorMessage": "An SDK's name must contain a letter, may have a single 'try-' or 'project-' prefix, must not chain prefixes, and the underlying name cannot be 'agent'." }, "channel": { "type": "string", "description": "Store channel used to retrieve the SDK at launch and refresh. Snap-like format: [/][/], or , or /. Risk is one of stable, candidate, beta, edge. Track is at most 28 characters; branch is at most 128 characters; neither may equal a risk name. Default is latest/stable. Only applies to SDKs from the Store; for try-, project-, and system SDKs the value has no effect but must still be well formed.", "pattern": "^(?:(?:[a-zA-Z0-9](?:[_.-]?[a-zA-Z0-9])*/)?(?:stable|candidate|beta|edge)(?:/[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9])?|[a-zA-Z0-9](?:[_.-]?[a-zA-Z0-9])*)$", "errorMessage": "Channel must look like [/][/] or []." }, "plugs": { "type": "object", "description": "Plug bindings and additional plug definitions for the SDK. Each key must start with a lowercase letter and contain only lowercase letters, digits, and hyphens between them.", "additionalProperties": false, "patternProperties": { "^[a-z](?:-?[a-z0-9])*$": { "description": "Either an inline plug definition (a mapping whose keys are interface-specific attributes such as interface, workshop-target, endpoint), a string naming the interface, null to use the key as the interface name, or a binding to another plug declared in the same workshop. To bind, provide a single bind key whose value is the target plug reference; bound plugs cannot define other attributes and cannot also appear in connections.", "properties": { "bind": { "type": "string", "description": "Plug reference in the form :. The target plug must use the same interface as this plug, must not itself be bound, and must not belong to the system SDK (so the portion must name a non-system SDK).", "pattern": "^(?!system:)(([a-z0-9]-?)*[a-z](-?[a-z0-9])*):[a-z](-?[a-z0-9])*$", "errorMessage": "Bind reference must follow the pattern :, where names a non-system SDK." } }, "if": { "type": "object", "required": [ "bind" ] }, "then": { "type": "object", "properties": { "bind": true }, "additionalProperties": false, "errorMessage": "When 'bind' is set, no other attributes are allowed on the plug." } } } }, "slots": { "type": "object", "description": "Additional slot definitions grafted onto the SDK by the workshop. Each key must start with a lowercase letter and contain only lowercase letters, digits, and hyphens between them. Each value is either an inline slot definition (a mapping with interface and any interface-specific attributes), a string naming the interface, or null to use the key as the interface name.", "patternProperties": { "^[a-z](?:-?[a-z0-9])*$": { "type": ["object", "string", "null"], "description": "Slot definition: an inline mapping with interface and any interface-specific attributes, a string naming the interface, or null to take the interface from the key." } }, "additionalProperties": false } }, "required": [ "name" ], "errorMessage": { "required": { "name": "Each SDK must specify a name." } }, "additionalProperties": false } }, "connections": { "type": "array", "description": "Explicit connections from plugs to slots, applied on top of auto-connection. Both endpoints must reference an SDK that is present in the workshop or is implicit (system, sketch), and must share the same interface. A plug that is bound elsewhere cannot also appear here.", "items": { "type": "object", "properties": { "plug": { "type": "string", "description": "Plug reference in the form :. The portion may be empty (for example, :ssh-agent) to refer to the system SDK.", "pattern": "^(([a-z0-9]-?)*[a-z](-?[a-z0-9])*)?:[a-z](-?[a-z0-9])*$", "errorMessage": "Plug reference must follow the pattern : (the portion may be empty for the system SDK)." }, "slot": { "type": "string", "description": "Slot reference in the form :. The portion may be empty (for example, :ssh-agent) to refer to the system SDK.", "pattern": "^(([a-z0-9]-?)*[a-z](-?[a-z0-9])*)?:[a-z](-?[a-z0-9])*$", "errorMessage": "Slot reference must follow the pattern : (the portion may be empty for the system SDK)." } }, "required": [ "plug", "slot" ], "errorMessage": { "required": { "plug": "Each connection must specify a plug.", "slot": "Each connection must specify a slot." } }, "additionalProperties": false } }, "actions": { "type": "object", "description": "Named shell scripts available via workshop run. Action names must start with a lowercase letter and contain only lowercase letters, digits, and hyphens between them. Each script body runs under bash as a login shell with errexit and pipefail set; positional parameters $@, $1, $2, ... receive the arguments passed after the workshop name.", "patternProperties": { "^[a-z](?:-?[a-z0-9])*$": { "type": "string", "description": "Shell script body executed by bash inside the workshop." } }, "additionalProperties": false, "errorMessage": "Action names must start with a lowercase letter and contain only lowercase letters, digits, and hyphens between them." } }, "required": [ "name", "base" ], "additionalProperties": false, "errorMessage": { "required": { "name": "The 'name' field is required.", "base": "The 'base' field is required." } } } ``` ## Examples Minimal workshop with one Store SDK and two actions: ```yaml name: golang base: ubuntu@22.04 sdks: - name: go channel: "1.26" actions: lint: | go vet golangci-lint run tests: go test "$@" ``` Workshop with an in-project SDK and a plug binding between SDKs: ```yaml name: go-dev base: ubuntu@22.04 sdks: - name: go channel: edge - name: project-cache plugs: data: bind: go:mod-cache ``` Workshop that grafts a plug and a slot onto its SDKs and adds explicit connections; besides using the fictional `tensorflow`, `imagenet` and `cuda` SDKs, it defines an additional slot under the `imagenet` SDK, a plug under `tensorflow`, and two connections: - One that connects the `tensorflow:images` plug to the newly defined `imagenet:images` slot. - Another that connects the `tensorflow:cuda` plug to the preexisting `cuda:libs`. ```yaml name: digits-cuda base: ubuntu@22.04 sdks: - name: tensorflow plugs: cuda: interface: mount workshop-target: /usr/local/cuda/lib64 - name: imagenet slots: images: interface: mount workshop-source: $SDK/images - name: cuda connections: - plug: tensorflow:cuda slot: cuda:libs - plug: tensorflow:images slot: imagenet:images ``` Workshop that pulls an SDK from the try area: ```yaml name: try-go base: ubuntu@24.04 sdks: - name: try-go ``` ## See also Explanation: - [Base image](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-base) - [In-project SDKs](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-in-project-sdk) - [SDKs](https://ubuntu.com/workshop/docs//explanation/index.md#exp-sdks) - [System SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-system-sdk) - [Testing and trying SDKs](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-test-try-sdk) - [Workshop definition](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition) Reference: - [SDK definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdk-definition.md#ref-sdk-definition) - [SDKcraft project definition](https://ubuntu.com/workshop/docs//reference/definition-files/sdkcraft-definition.md#ref-sdkcraft-definition) - [workshop info](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-info) # workshop-status.md # Workshop status diagrams During its lifecycle, a workshop goes through a number of states, which we call *statuses* to distinguish them from SDK states. The following partial diagrams represent each state with the commands that cause the workshop to transition to a different status. ## Off Always the starting point, where the workshop exists solely as a [definition file](https://ubuntu.com/workshop/docs//reference/definition-files/workshop-definition.md#ref-workshop-definition) in the project directory; there is no container yet. ## Ready The workshop was successfully launched from the definition file; its underlying container is linked to the project directory, up and ready to do some work. ## Stopped The underlying container was stopped but is still linked to the project directory. ## Waiting The workshop is paused in the middle of a change to allow for interactive debugging; only a few commands will be accepted. ## Pending An intermediate state while the workshop is being updated or changing its status; only a few commands will be accepted. ## Error The workshop failed at some stage, and its underlying container became nonoperational. ## See also Explanation: - [Workshop status](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-status) Reference: - [Command-line interfaces](https://ubuntu.com/workshop/docs//reference/index.md#ref-cli) # workshop.md # workshop (CLI) The **workshop** utility exposes the following commands, each with its own set of options, and also has a number of global flags: ## workshop actions List the named actions defined in a workshop. ### Usage ```console $ workshop actions [] [flags] ``` ### Description This command enumerates all actions in the workshop, printing a YAML map. ### Examples List actions for the “nimble” workshop in the current project directory: ```console $ workshop actions nimble ``` The name is optional if the project has only one workshop: ```console $ workshop actions ``` ## workshop changes List recent changes to the workshops in a project. ### Usage ```console $ workshop changes [flags] ``` ### Description Any substantial operation on a workshop is a change that consists of tasks; the command lists details of recent changes for all workshops within a project. For each change, it prints the following details: - ID: Uniquely identifies the change within the project - Status: Reflects the change’s progress and affects the workshop’s status - Spawn: Tells when the change was started - Ready: Tells when the change was successfully finished, if at all - Summary: Lists actions, affected workshops, other information Notes: - Only successful changes display values in the “Ready” column - To investigate the details of a specific change, use “workshop tasks” instead ### Examples List changes for all workshops in the current project directory: ```console $ workshop changes ``` ### Flags ### See also Reference: - [workshop info](#ref-workshop-info) - [workshop list](#ref-workshop-list) ## workshop connect Connect a plug to a slot. ### Usage ```console $ workshop connect /: [/][:] [flags] ``` ### Description This command connects a plug to a target slot that is specified as the second argument or deduced from the context. - If the second argument is omitted entirely, the target is assumed to be /system:; and come from the first argument. - If the second argument only names the slot itself, the target is /system:; comes from the first argument. - If the second argument omits the name, the target slot is the one that uses the same interface as the , regardless of the slot’s name. However, if there are several slots that use the same interface, the command fails. - If the target slot is compatible with the plug, the command attempts to connect them and returns the result. Notes: - To be compatible, the plug and the slot must use the same interface. - Multiple plugs can be connected to the same slot, but not vice versa. - The “workshop connections” output will list the connection as “manual”. ### Examples Connect the “mod-cache” mount interface plug of the “go” SDK under the “nimble” workshop in the current project directory: ```console $ workshop connect nimble/go:mod-cache :mount ``` A full version of the command that also lists the target SDK (“system”): ```console $ workshop connect nimble/go:mod-cache nimble/system:mount ``` ### Flags ## workshop connections List interface connections. ### Usage ```console $ workshop connections [] [flags] ``` ### Description This command lists the connections between interface plugs and slots for the entire project or a single workshop within it. Each line represents a connection between a plug and a slot via an interface; additional notes, including specific plug bindings, are provided as needed. Notes: - The output lists connections created with “workshop connect” as “manual”. - The “--all” option needn’t be used with an argument; if a workshop is supplied, disconnected plugs are also listed. ### Examples List connections for the workshop “nimble” in the current project directory: ```console $ workshop connections nimble ``` List connections for all workshops in the current project directory: ```console $ workshop connections ``` ### Flags ## workshop disconnect Disconnect a plug or a slot. ### Usage ```console $ workshop disconnect /: [/]:[] [flags] ``` ### Description This command disconnects a plug from its slot, or a slot from all its plugs. - A single argument can be a fully qualified plug or slot reference; with two arguments, the first one is the plug, and the second one is the slot. - If the second argument only names the slot itself, the target is /system:; comes from the first argument. - If the second argument only names the workshop and SDK, the target is /:; is the interface in the plug’s definition. Notes: - After an auto-connected plug is thus disconnected, it is reconnected during “workshop refresh” only if the “--forget” option was used with “workshop disconnect”. ### Examples Disconnect the “mod-cache” mount interface plug of the “go” SDK under the “nimble” workshop in the current project directory: ```console $ workshop disconnect nimble/go:mod-cache ``` A full version of the same command that lists the target SDK (“system”) and slot (“mount”): ```console $ workshop disconnect nimble/go:mod-cache nimble/system:mount ``` Disconnect all plugs connected to the “mount” slot of the “system” SDK under the “nimble” workshop in the current project directory: ```console $ workshop disconnect nimble/system:mount ``` ### Flags ## workshop exec Run an ad-hoc shell command in a workshop. ### Usage ```console $ workshop exec [flags] [] [--] ... ``` ### Description The “exec” subcommand runs an arbitrary command in the specified workshop, waiting for it to complete. If a timeout elapses before that, it’s terminated. Use “exec” for one-off shell commands typed on the command line. To invoke a named script defined in the workshop’s “actions:” section instead, use “workshop run”. To accept an “exec” command, the workshop must be “Ready” or “Waiting”. A command can run in two modes that determine how it handles standard streams: - Interactively (for shell sessions) - Non-interactively (for scripts) To set the mode explicitly, use “-i” or “-I”. If neither is supplied, “exec” deduces the mode based on the nature of its own streams: - If stdin and stdout are terminals, the mode is interactive - Otherwise, it’s non-interactive To separate the “exec” subcommand from the command itself, use a separator ( *--*). If you omit the separator, “exec” treats its first argument as the workshop name. If the project has no such workshop and the shell is interactive, the argument is treated as a command to run in the default workshop. Notes: - To start a workshop before running commands in it, use “workshop start”. - You can set the working directory, environment variables, user and group ID for running the command in the workshop; reasonable defaults are provided. ### Examples Run the “go build main.go” command under the “nimble” workshop in the current project directory: ```console $ workshop exec nimble -- go build main.go ``` A similar command that sets an environment variable and the working directory: ```console $ workshop exec --env GO111MODULE=off -w /project nimble -- go build -x ``` Run a command as root (the default is “workshop”): ```console $ workshop exec --uid 0 nimble id ``` Run a custom interactive shell: ```console $ workshop exec -I nimble sh ``` If the project has only one workshop, the workshop name is optional: ```console $ workshop exec -- sh ``` If the command doesn’t overlap with a workshop name and the shell is interactive, the separator is also optional: ```console $ workshop exec sh ``` ### Flags ## workshop info Print the current status and details of a workshop as YAML. ### Usage ```console $ workshop info [] [flags] ``` ### Description This command outputs the basic settings, current status and individual SDK details for a workshop, formatting them as YAML. Specifically, it prints: - Essential workshop attributes, such as name, base and project directory - Current status (e.g. ‘Ready’, ‘Pending’, ‘Stopped’) and notes for the workshop - Individual SDK details, such as name, channel, installation date and revision - Currently connected mount interface plugs Notes: - Avoid assumptions based on SDK channels: “latest/stable” may be neither. ### Examples List details for the “nimble” workshop in the current project directory: ```console $ workshop info nimble ``` The name is optional if the project has only one workshop: ```console $ workshop info ``` ## workshop init Create a new workshop definition in the project directory. ### Usage ```console $ workshop init --sdks [--base ] [flags] ``` ### Description Create a new workshop definition file in the project’s .workshop/ directory. The NAME argument sets the workshop name. The command creates a named workshop file at .workshop/.yaml. This fails if a workshop with the same name already exists. SDKs are specified as a comma-separated list. Each SDK entry can optionally include a channel using the / syntax (e.g., “go/1.26/stable”). ### Examples Create a workshop called “dev” with the Go and UV SDKs: ```console $ workshop init dev --sdks go,uv ``` Create a workshop with a specific SDK channel: ```console $ workshop init dev --sdks go/1.26/stable ``` Create a workshop using a specific base: ```console $ workshop init dev --sdks go --base ubuntu@22.04 ``` ### Flags ## workshop launch Construct one or many workshops using their definitions. ### Usage ```console $ workshop launch ... [flags] ``` ### Description This command constructs the workshops listed as arguments by going over their definitions and installing their components. For each workshop, it: - Checks the workshop definition and identifies necessary actions - Retrieves the required components, such as base and SDKs - Runs SDK setup hooks to initialize the working state - On success, ties the workshop to the project and starts it The “--wait-on-error” option pauses the launch if an error occurs. Thus, you can fix the error and resume the operation or abort and revert it. This option can only be used with a single workshop. If multiple workshops are listed and an error occurs, the operation is aborted and no workshops are constructed. Also, if you change the workshop definition while fixing the error, you must abort the operation and restart from scratch. Notes: - Names listed as arguments must match respective “name:” values in definitions. - To update an existing workshop, use “workshop refresh” instead. - SDKs are installed in the order they are listed in the definition. ### Examples Launch the “nimble” and “jazzy” workshops in the current project directory: ```console $ workshop launch nimble jazzy ``` The name is optional if the project has only one workshop: ```console $ workshop launch ``` ### Flags ## workshop list List project workshops. ### Usage ```console $ workshop list [flags] ``` ### Description This command enumerates all workshops in the project, printing a compact list: - Project: Absolute pathname of the project where this workshop belongs - Workshop: Workshop name, as set by its definition - Status: Workshop status, such as “Off”, “Ready”, “Pending” and so on - Notes: Internal remarks on the overall state of the workshop The “--global” option lists all workshops from all projects in the system; however, it doesn’t include any that are “Off”. Notes: - For details of a single workshop, use “workshop info” instead. ### Examples List the workshops in the current project directory: ```console $ workshop list ``` List the globally registered workshops: ```console $ workshop list --global ``` ### Flags ## workshop okay Acknowledge listed warnings. ### Usage ```console $ workshop okay [flags] ``` ### Description This command acknowledges all warnings listed previously by the “workshop warnings” command. ### Examples Acknowledge the globally registered warnings across all workshops (must run after “workshop warnings”): ```console $ workshop okay ``` ## workshop refresh Update workshops according to their definitions. ### Usage ```console $ workshop refresh [--abort|--continue|--wait-on-error] ... [flags] ``` ### Description This command updates the workshops listed as arguments. For each workshop, it checks the workshop definition and applies any required updates to the base image, SDKs, and interface connections: - Connections added at runtime with “workshop connect” are dropped, and the workshop returns to its definition’s auto-connect defaults. - A connection removed with “workshop disconnect” without “--forget” stays disconnected after refresh. The “--wait-on-error” option pauses the refresh if an error occurs. Thus, you can fix the error and resume the operation or abort and revert it. This option can only be used with a single workshop. If multiple workshops are listed and an error occurs, the operation is aborted and reverted for all of them. Also, if you change the workshop definition while fixing the error, you must abort the operation and restart from scratch. Notes: - The workshop must be “Ready” to be refreshed. Throughout the refresh, all affected workshops remain unavailable for other changes. - Updated and newly added SDKs are installed in the order they are listed in the workshop definition. - To construct a newly defined workshop, use “workshop launch” instead. ### Examples Refresh the “nimble” and “jazzy” workshops in the current project directory: ```console $ workshop refresh nimble jazzy ``` The name is optional if the project has only one workshop: ```console $ workshop refresh ``` Refresh workshop, but pause on any errors (won’t accept multiple workshops): ```console $ workshop refresh --wait-on-error ``` After refresh paused on error, abort the operation: ```console $ workshop refresh --abort ``` After refresh paused on error and the workshop was fixed, continue the operation: ```console $ workshop refresh --continue ``` ### Flags ## workshop remount Mount a new source location to the mount interface plug’s target. ### Usage ```console $ workshop remount /: [flags] ``` ### Description This command mounts a new source location on the host to the target directory of the specified mount interface plug, qualified by the SDK name. Specifically, it does the following: - Attempts the mount operation atomically; this normally succeeds if the new source is either a non-existing directory or an empty directory on the same file system as the current source. - Otherwise, performs the mount operation only if the workshop is ‘Stopped’ to prevent data corruption. Notes: - To stop the workshop, use “workshop stop”. - “workshop info” lists any connected mount interface plugs for the workshop. - “workshop refresh” mounts the last source set by “workshop remount”, if any. - During “workshop remove”, non-default sources set by “workshop remount” aren’t removed. ### Examples Remount the “mod-cache” mount interface plug of the “go” SDK under the “nimble” workshop in the current project directory to “~/new-cache-mount/” on the host: ```console $ workshop remount nimble/go:mod-cache ~/new-cache-mount ``` ### Flags ## workshop remove Remove one or many workshops. ### Usage ```console $ workshop remove ... [flags] ``` ### Description This command removes the workshops listed as arguments. For each workshop, it: - Checks that the workshop isn’t “Off” or “Pending” - Stops the workshop if it’s not already “Stopped” - Deletes the workshop but preserves its definition Notes: - If any listed workshop is “Off” or “Pending”, none are removed. - To rebuild a removed workshop from scratch, use “workshop launch”. - For mount interface plugs, non-default sources set by “workshop remount” aren’t removed. ### Examples Remove the “nimble” and “jazzy” workshops in the current project directory: ```console $ workshop remove nimble jazzy ``` The name is optional if the project has only one workshop: ```console $ workshop remove ``` ## workshop restore Restore workshops to the state of the last launch or refresh. ### Usage ```console $ workshop restore [flags] ... ``` ### Description This command restores the container filesystem of the workshops listed as arguments to the point of the last launch or refresh, then resets the interface connections to default settings: - Connections added at runtime with “workshop connect” are dropped, and the workshop returns to its definition’s auto-connect defaults. - A connection removed with “workshop disconnect” without “--forget” stays disconnected after restore. Notes: - The workshop must be “Ready” to be restored. - Multiple workshops can be restored in a single command invocation; the operation is transactional, so if any workshop fails to restore, all are reverted. - To update an existing workshop instead of reverting changes, use “workshop refresh”. ### Examples Restore the “nimble” and “jazzy” workshops in the current project directory: ```console $ workshop restore nimble jazzy ``` The name is optional if the project has only one workshop: ```console $ workshop restore ``` ### Flags ## workshop run Run a named action from the workshop definition. ### Usage ```console $ workshop run [flags] [] [--] ... ``` ### Description The “run” subcommand runs an action specified in the workshop definition file, waiting for it to complete. If a timeout elapses before that, it’s terminated. Use “run” to invoke a named action defined in the workshop’s “actions:” section. To list available actions, use “workshop actions”. To run an ad-hoc shell command instead, use “workshop exec”. To accept a “run” command, the workshop must be “Ready” or “Waiting”. A command can run in two modes that determine how it handles standard streams: - Interactively (for shell sessions) - Non-interactively (for scripts) To set the mode explicitly, use “-i” or “-I”. If neither is supplied, “run” deduces the mode based on the nature of its own streams: - If stdin and stdout are terminals, the mode is interactive - Otherwise, it’s non-interactive To separate the “run” subcommand from the action and its arguments, use a separator ( *--*). If you omit the separator, “run” treats its first argument as the workshop name. If the project has no such workshop and the shell is interactive, the argument is treated as an action to run in the default workshop. Any trailing arguments are forwarded to the action as positional parameters, so action scripts can consume them with standard shell expansions. Notes: - To start a workshop before running actions in it, use “workshop start”. - You can set the working directory, environment variables, user and group ID for running the action in the workshop; reasonable defaults are provided. ### Examples Run the “build” action under the “nimble” workshop in the current project directory: ```console $ workshop run nimble build ``` A similar command that sets an environment variable and the working directory: ```console $ workshop run --env GO111MODULE=off -w /project nimble -- build ``` If the project has only one workshop, the workshop name is optional: ```console $ workshop run -- build ``` If the action doesn’t overlap with a workshop name and the shell is interactive, the separator is also optional: ```console $ workshop run build ``` Forward arguments to an action that consumes them (for example, `tests: go test "$@"` in the workshop definition): ```console $ workshop run dev -- tests -run TestFoo ./pkg/... ``` ### Flags ## workshop shell Start an interactive terminal session for the workshop. ### Usage ```console $ workshop shell [] [flags] ``` ### Description The “shell” subcommand runs an interactive terminal session in the specified workshop. To accept a “shell” command, the workshop must be “Ready” or “Waiting”. Notes: - To start a workshop before running a terminal session, use “workshop start”. - The subcommand is a shorthand for “workshop exec”; it launches the login shell for “workshop”, the default non-privileged user in a workshop. ### Examples Open the default login shell of the “workshop” user into the “nimble” workshop in the current project directory: ```console $ workshop shell nimble ``` The name is optional if the project has only one workshop: ```console $ workshop shell ``` ## workshop sketch-sdk Customize a workshop. ### Usage ```console $ workshop sketch-sdk [--stash|--restore|--eject|--remove] [] [flags] ``` ### Description The command opens the sketch SDK template in the default text editor. Add customizations by editing the template, then save and exit the editor to apply the changes to the workshop. The “--stash” and “--restore” options respectively stash the SDK, reversing the changes, and quickly restore it to the workshop. To make these customizations persistent, run “workshop sketch-sdk --eject”. This saves the SDK definition under .workshop/ in the project directory, so it can be committed to your repository. The sketch SDK is intended for experiments and prototyping iterations. Notes: - You can only have one sketch SDK per workshop at a time. - Run “workshop info” to list all SDKs currently installed in the workshop, including the sketch SDK if present. ### Examples Edit the sketch SDK definition for the “nimble” workshop and apply it after saving by automatically refreshing the workshop: ```console $ workshop sketch-sdk nimble ``` Save the sketch SDK for the “nimble” workshop as a project SDK named “tools”: ```console $ workshop sketch-sdk nimble --eject --name tools ``` Stash the sketch SDK, temporarily reverting the changes in the workshop: ```console $ workshop sketch-sdk nimble --stash ``` ### Flags ## workshop sketches List project sketch SDKs. ### Usage ```console $ workshop sketches [flags] ``` ### Description This command enumerates all sketches in the project, printing a compact list: - Project: absolute pathname of the project - Workshop: workshop name, as set in its definition - Rev: sketch SDK revision, if present - Notes: current, stashed, or both ### Examples List the sketches in the current project directory: ```console $ workshop sketches ``` ### Flags ## workshop start Start one or many workshops. ### Usage ```console $ workshop start ... [flags] ``` ### Description This command activates the workshops listed as arguments. For each one, it: - Makes sure the workshop was actually launched - Activates the workshop for use and sets it to ‘Ready’ If multiple workshops are listed and an error occurs, the operation is aborted and no workshops are started. Notes: - If a workshop is already started or wasn’t yet launched, an error occurs. - When interrupted, the command attempts to gracefully revert its actions. - To stop a started workshop, use “workshop stop”. ### Examples Start the “nimble” and “jazzy” workshops in the current project directory: ```console $ workshop start nimble jazzy ``` The name is optional if the project has only one workshop: ```console $ workshop start ``` ## workshop stop Stop one or many workshops. ### Usage ```console $ workshop stop ... [flags] ``` ### Description This command deactivates the workshops listed as arguments. For each one, it: - Makes sure the workshop was actually started or is already stopped - Deactivates the workshop and sets it to ‘Stopped’ If multiple workshops are listed and an error occurs, the operation is aborted and no workshops are stopped. Notes: - If a workshop wasn’t yet started or even launched, an error occurs. - When interrupted, the command attempts to gracefully revert its actions. - To start a stopped workshop, use “workshop start”. ### Examples Stop the “nimble” and “jazzy” workshops in the current project directory: ```console $ workshop stop nimble jazzy ``` The name is optional if the project has only one workshop: ```console $ workshop stop ``` ## workshop tasks List tasks for a specific change. ### Usage ```console $ workshop tasks [] [flags] ``` ### Description Any substantial operation on a workshop is a change that consists of tasks; the command lists individual tasks that comprise a specific change. For each task, it prints the following details: - Status: Reflects the task’s progress and affects the status of the change - Duration: Tells how long the task has been running - Summary: Lists actions, affected SDKs and workshops, other information Notes: - The command may print additional log details for tasks that store them - To investigate recent changes in a project, use “workshop changes” instead ### Examples List the tasks under change ID 42: ```console $ workshop tasks 42 ``` List the tasks under the most recent change to the project: ```console $ workshop tasks ``` ### Flags ## workshop warnings List warnings. ### Usage ```console $ workshop warnings [flags] ``` ### Description This command lists the warnings that were reported to the system. All warnings listed by “workshop warnings” can be acknowledged with the “workshop okay” command. Acknowledged warnings aren’t listed by “workshop warnings” unless they occur again after their cooldown period has elapsed or the “--all” option is used. Also, warnings expire automatically; expired warnings are not listed. ### Examples List the globally registered warnings across all workshops: ```console $ workshop warnings ``` ### Flags ## Shell completion The **workshop** CLI ships completion scripts for Bash, Zsh, and Fish. #### NOTE When **Workshop** is installed via snap, completion for Bash, Zsh, and Fish is enabled automatically; no further configuration is needed for these shells. To enable completion for the current shell session, source the script for your shell. Bash: ```console $ source <(workshop completion bash) ``` Zsh: ```console $ source <(workshop completion zsh) ``` Fish: ```console $ workshop completion fish | source ``` For per-shell installation that persists across new sessions, follow the instructions printed by the shell-specific help command. For example, for Bash: ```console $ workshop completion bash --help ``` ### What gets completed Beyond subcommand and flag names, the **workshop** CLI completes arguments and flag values dynamically: - Workshop names, filtered by lifecycle status per command; for example, **workshop start** lists only *Stopped* workshops, while **workshop stop** lists only *Ready* ones. - Plugs and slots for **workshop connect** and **workshop disconnect**: the first argument completes available plugs, the second completes valid slots for the chosen plug. - Recent change IDs for **workshop tasks**. ## See also Explanation: - [workshop (CLI)](https://ubuntu.com/workshop/docs//explanation/workshops/workshop-cli.md#exp-workshop-cli) # workshopctl-cli.md # workshopctl (CLI) **workshopctl** is a small in-workshop utility that [SDK hooks](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-sdk-hooks) invoke to report SDK state back to the **Workshop** daemon over a restricted socket. It runs inside a running workshop, not on the host, and is not intended for end users to call directly. There is one category of commands: | Actions | Commands | What they do | |-------------------|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Report SDK health | **set-health** | Let the daemon know whether the SDK is `okay`,
`waiting`, or in an `error` state,
with an optional machine-readable error code
and a human-readable message. | #### NOTE **workshopctl** only works from an SDK hook context, where the daemon supplies a context cookie via the `WORKSHOP_COOKIE` environment variable. Running it from an interactive shell returns `cannot invoke workshopctl operation commands ... from outside of a workshop`. ## See also Explanation: - [sdk (CLI)](https://ubuntu.com/workshop/docs//explanation/sdks/sdk-cli.md#exp-sdk-cli) - [workshop (CLI)](https://ubuntu.com/workshop/docs//explanation/workshops/workshop-cli.md#exp-workshop-cli) Reference: - [Command-line interfaces](https://ubuntu.com/workshop/docs//reference/index.md#ref-cli) - [SDK hooks](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-sdk-hooks) Tutorial: - [Get started with workshops](https://ubuntu.com/workshop/docs//tutorial/part-1-get-started.md#tut-get-started) # workshopctl.md # workshopctl (CLI) SDKs use the **workshopctl** tool when reporting to the workshop; to invoke a subcommand, add it to your [SDK hook](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-sdk-hooks). ## workshopctl set-health Report the health of the SDK. ### Usage ```console $ workshopctl set-health [--code=] [] ``` ### Description This command is essential for the `check-health` hook that runs after launch or refresh operations in a workshop. The arguments are as follows: | Placeholder | Required | Value | |---------------|--------------------------------------------------------------------------------|--------------------------------------------------------------| | `` | Required | Can be `okay`, `waiting` or `error`. | | `` | Required when `` is `waiting` or `error`;
not allowed with `okay`. | Arbitrary string explaining the status;
7–70 characters. | ### Examples Report an error with a code and a message; note only the message is quoted: ```console $ workshopctl set-health --code=missing-cuda error "CUDA libraries not found" ``` ### Flags ## See also Explanation: - [workshopctl (CLI)](https://ubuntu.com/workshop/docs//explanation/sdks/workshopctl-cli.md#exp-workshopctl-cli) Reference: - [SDK hooks](https://ubuntu.com/workshop/docs//reference/sdks.md#ref-sdk-hooks) - [workshop (CLI)](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-cli) # workshops.md # Workshop internals This topic speaks about what goes into building and running a workshop. ## What a workshop is A workshop is a development environment running in a container, mapping your project to its contained dependencies. It is fully described in a [definition file](https://ubuntu.com/workshop/docs//reference/definition-files/workshop-definition.md#ref-workshop-definition). **Workshop** currently uses LXD as its container engine, communicating to it via the socket-activated **workshopd** backend, but that is an implementation detail and potential subject to change. When you launch a workshop, you create and start a container with a base image. The image is downloaded from the container engine server and cached locally. On top of the base image, one or more SDKs are usually applied, which provide tools for development and runtime tasks. SDKs are downloaded from the SDK Store that is specific to **Workshop**. SDKs are installed in the order dictated by the workshop definition: first `system`, then user-listed SDKs, and finally `sketch`. Each SDK’s `setup-base` hook runs immediately after that SDK is installed; this serves to customize the workshop and prepare each SDK for use before proceeding further. After all SDKs are installed, the following hooks execute sequentially in the same deterministic order: `setup-project` hooks run first to prepare the project environment, followed by `check-health` hooks to verify the workshop is ready for use. The host user running **Workshop** is mapped to the default workshop user, named `workshop` in the container, with `uid=1000` and `gid=1000`. This ensures that files and directories changed by the `workshop` user inside the container map back to the same user on the host, preserving consistent ownership and permissions. ## User sessions and ID mapping By default, all commands that execute something inside a workshop (**exec**, **run**, **shell**) run login shells as `uid=1000` and `gid=1000`, unless you explicitly change the IDs. This ensures files created inside the container map back to the host user. By default, `$XDG_RUNTIME_DIR` is set to `/run/user/1000/`, and a user-scoped session bus address is created at `/run/user/1000/bus`. Using other IDs can break sessions. Finally, it’s worth mentioning that having an active session doesn’t prevent **refresh** or any other change from running. However, you may need to restart the session to see the effect. ## How workshops are built At the container launch, **Workshop** does the following: - Reads the workshop definition to see which base image and SDKs are needed. - Fetches the required images and SDKs from the image server and the Store, if they aren’t already cached locally. - Reads definitions of local SDKs, such as the sketch SDK, and copies them to a read-only location. - Spins up the container with mapped user and group IDs, configures basic devices like the root disk and network bridge. - Installs the SDKs, then runs their `setup-base` hooks in the container. - Configures the container’s time zone to match the host. - Maps the project directory on the host to `/project` in the container; this allows to transparently work on the host-based files using the tools provided by the SDKs. - Sets up interface plugs and slots defined by the workshop and its SDKs for extra devices and capabilities. - Runs `setup-project` and `check-health` hooks for all SDKs. Thus, the container is built, or launched in **Workshop** terms. All subsequent start and stop operations affect an already built container; any rebuilds occur only with a refresh. After a successful start, you have a running container named for your workshop, accessible via the regular container engine capabilities. From a user’s perspective, it behaves like a custom Ubuntu environment tailored to a specific project. ## How workshops are updated **Workshop** has a refresh manager that tracks all updates in a workshop (changes to the base image, adding or removing SDKs, updates to the definition) and builds an update plan to decide whether a refresh is needed. Some changes don’t cause a refresh by their nature; for example, updated actions in the definition are copied inside the workshop. Larger ones, like switching base images or changing the SDK layout, trigger the refresh mechanism: - A snapshot of the current container is stashed as a fallback. - A new container is built based on the updated setup. - If the build succeeds, the old container is dropped. If it fails, the system reverts to the previous snapshot. - After a successful refresh, the stash is cleaned up. This prevents broken states during major refreshes. To explicitly revert to the state after the last successful launch or refresh, use the **workshop restore** command. It restores the container filesystem from the most recent snapshot and resets all connections and mounts to their defaults. Unlike the automatic rollback during a failed refresh, **restore** is a deliberate user action to discard all changes made to a workshop since it was last set up. ## Storage pools and drivers **Workshop** stores its containers and data on a storage pool. On Linux, **Workshop** uses ZFS, while on Windows Subsystem for Linux it automatically uses Btrfs. This approach consolidates container images, **apt** caches, SDKs and other workshop content under a single system. If you need more space or different performance, you can resize or tune the storage pool (it’s named `workshop`), using the **lxc storage** command as suggested in this [LXD documentation section](https://documentation.ubuntu.com/lxd/latest/howto/storage_pools/). However, day-to-day usage requires little manual intervention. #### ATTENTION Don’t use storage driver utilities (such as **zfs** or **btrfs**) directly to alter the LXD-managed storage pool, as this may cause issues with LXD. Additionally, **Workshop** ensures the LXD storage pool itself is at least 5 GiB. Otherwise, LXD allocates 20% of the available disk space by default. If the total disk space is under 14 GiB, this would result in a pool size of only about 2 GiB per workshop, making it more likely to run out of space. ## Details of the **apt** cache To speed up repeated software installations, each workshop maintains a package cache at `/var/cache/apt/archives`. Because the container is single-user, only the mapped user and root can access that cache. When a workshop is removed, the **apt** cache directory is removed as well. ## Interfaces As a reminder, **Workshop** enforces resource access with plugs and slots. A plug requests a resource (for example, the GPU or a socket), and a slot provides it. However, only the default workshop user can access the host resources that interfaces expose inside the container. Other users may exist (for example, those hard-coded in an SDK), but they do not have that access and are not intended to. In particular, this includes interface-based SSH or desktop sessions, which are also limited to the default workshop user. ## See also Explanation: - [Workshop concepts](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-concepts) - [Workshop status](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-status) Reference: - [workshop (CLI)](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-cli) - [Workshop status diagrams](https://ubuntu.com/workshop/docs//reference/workshop-status.md#ref-workshop-status) # workshops.md # Workshops The eponymous *workshop* is the core idea behind **Workshop**. ## Core concepts A workshop is a container-based development environment defined in YAML and hosted by LXD. Projects are the directories that hold workshop definitions and mount inside the running containers: * [Workshop concepts](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md) * [Projects](https://ubuntu.com/workshop/docs//explanation/workshops/projects.md) * [Multi-workshop patterns](https://ubuntu.com/workshop/docs//explanation/workshops/multi-workshop-patterns.md) ## Operations and tooling **Workshop** tracks state through a system of changes and tasks, and exposes its functionality through the **workshop** CLI: * [Changes, tasks](https://ubuntu.com/workshop/docs//explanation/workshops/changes-tasks.md) * [workshop (CLI)](https://ubuntu.com/workshop/docs//explanation/workshops/workshop-cli.md)