Contribute to development

A code change to Workshop moves from a local development environment, through testing and review, to a merged pull request.

Set up your work environment

Workshop has a client-server architecture. Its workshopd daemon exposes a RESTful API (see internal/daemon/api.go) to the clients.

Workshop also develops itself in a workshop. The repository is a project whose .workshop/dev.yaml definition describes a workshop named dev. It carries the Go toolchain and the linters, pinned to the versions the project checks against, and packages them as actions.

Your work is split across two environments:

Environment

What runs there

The dev workshop

Go builds, golangci-lint, shellcheck, unit tests and coverage, and the documentation build, preview, and checks.

The host

workshopd, the spread suites, and the LXD integration tests.

workshopd connects to LXD and expects the ZFS storage driver, neither of which the dev workshop ships, so the host-side commands run outside it.

Launch the dev workshop

Install Workshop and LXD first (see Install Workshop), then launch the workshop from the repository root:

$ workshop launch dev

The definition pins the toolchain to the versions the project checks against:

  • The go SDK tracks 1.26/stable, matching the go directive in go.mod.

  • golangci-lint is pinned to v2.11.3, matching the rev in .pre-commit-config.yaml; the linting workflow in .github/workflows/lint.yaml tracks the same v2.11 minor version.

Linting in the workshop therefore uses the same version as the linting workflow, without installing the linters on your host.

The tooling comes from an in-project SDK in .workshop/tools/, which the definition lists as project-tools; the project- prefix selects the in-project source (see SDK entry). Its hooks install make, shellcheck, golangci-lint, and snapd-testing-tools, and report the workshop unhealthy when golangci-lint is missing (see Talking back with workshopctl). The SDK also slots a tunnel that the system SDK plugs to publish the documentation preview on the host at 127.0.0.1:8000 (see How to forward ports with tunneling).

List the available actions and run one:

$ workshop actions dev
$ workshop run dev lint

Because dev is the only definition in .workshop/, you can omit its name. See workshop actions and workshop run.

Open an interactive shell with workshop shell, run a single command with workshop exec, and apply edits to .workshop/dev.yaml or to the SDK in .workshop/tools/ with workshop refresh.

Run the daemon on the host

The recommended way to run the current sources is the go tool try development tool wired into go.mod:

$ 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. Run go tool try again from inside the shell to rebuild and restart workshopd in place.

To run workshopd directly:

$ 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:

$ export WORKSHOP=~/workshop
$ workshop list

Install Spread

Spread is the end-to-end testing tool for Workshop. It launches its own LXD containers, so install and run it on the host:

$ 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:

$ spread -h

Run the documentation end-to-end test suites with:

$ spread tests/docs-tutorial/
$ spread tests/docs-how-to/

Choose a task

Work is tracked in the project’s issue tracker. Browse the open issues and comment on one to signal that you’re taking it on.

For a small, self-contained fix, you can open a pull request directly. For a larger or more involved change, open an issue first to agree on the approach with the maintainers before you invest time in the implementation.

Draft your work

Create a branch

Name your branch after the type of change and a short description. Workshop uses the commit type as a branch prefix, for example:

  • feat/workspace-start

  • fix/spread-tests-github

  • chore/update-lxd

Develop

Keep each change focused and reversible. When a decision might be costly to reverse, state the rationale in the pull request description so reviewers can follow the reasoning and collaborate on it.

Workshop follows a set of Go conventions for naming, error handling, error messages, and code structure. See the Workshop Go coding style guide for the full set of patterns and their rationale.

Test

Run the checks before submitting a pull request. The dev workshop runs them with the tool versions the project pins (see Launch the dev workshop); each check also has a host-native equivalent if you have the tools installed.

Workshop tests use gocheck, which integrates with go test. Run the unit tests with coverage:

$ workshop run dev cover

Or on the host:

$ go test ./...
$ go test -covermode=count -coverpkg=./... -coverprofile=coverage.out ./...

To run a single test or suite, name the package first:

$ workshop exec dev -- go test <PACKAGE> -check.f <TEST-NAME|SUITE-NAME>

To control the coverage scope and mode:

$ workshop exec dev -- go test -coverpkg=<./...|PACKAGE> \
    -covermode=<set|count|atomic> \
    -coverprofile=<OUTPUT-FILE> <./...|PACKAGE>

The cover action writes its profile to coverage.out. Because the project directory is mounted into the workshop at /project/, you can render the profile on the host:

$ go tool cover -html=<OUTPUT-FILE> -o <OUTPUT-HTML>

The output flag can be omitted to open in the default browser:

$ go tool cover -html=coverage.out

Formatting and common pitfalls are checked with golangci-lint. Lint the sources in both the full and the incremental configuration:

$ workshop run dev lint

Or on the host:

$ golangci-lint run
$ golangci-lint run --new-from-rev='HEAD~' --config=.golangci.incremental.yaml

Some issues can be fixed automatically:

$ workshop exec dev -- golangci-lint run --fix

Or on the host:

$ golangci-lint run --fix

Check the shell scripts and the spread task definitions:

$ workshop run dev shellcheck

If pre-commit is available, git can run the linters on every commit using the same pinned golangci-lint:

$ pre-commit install

The LXD integration tests sit behind a build tag and drive LXD directly, so run them on the host:

$ go test -tags=integration ./internal/workshop/lxd/tests/integration/

Run the end-to-end suites with Spread, also on the host:

$ spread tests/<TEST-PATH-NAME>

Integration tests run through spread create the coverage profile automatically, but the artifacts need to be collected from the VM with the -artifacts flag:

$ spread -artifacts=<PATH-TO-DEST> tests/integration/

Document your work

Update the documentation to match your change: new behavior, changed flags, or removed features all belong in the docs. See Contribute to this documentation for how to write, build, and test them.

Commit

Workshop commit messages differ from conventional commits in capitalization:

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 (fix, feat, and so on); these are used for branch naming instead.

Review with the team

Send for review

Open a pull request against the repository. Fill in the pull request template checklist, which largely reiterates the expectations covered here.

Address quality concerns

Your pull request runs a set of automatic checks for linting, unit tests, end-to-end tests, and security scanning. For the full catalog of workflows, see CI/CD.

After receiving review comments, optimize for commit history clarity. Address them with fixup commits and rebase using autosquash when reasonable.

Wrap up the review

Once the checks pass and a maintainer approves the change, it’s merged into the target branch. Keep an eye on the pull request in case follow-up questions come up.

Get help and support

If you get stuck, ask on the issue you’re working on, or open a new one in the issue tracker.