# 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.

# 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 `[