<a id="contributing-development"></a>

# 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](https://ubuntu.com/workshop/docs//explanation/workshops/projects.md#exp-projects)
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](https://ubuntu.com/workshop/docs//explanation/workshops/concepts.md#exp-workshop-definition-actions).

Your work is split across two environments:

| Environment        | What runs there                                                                                                                  |
|--------------------|----------------------------------------------------------------------------------------------------------------------------------|
| The `dev` workshop | Go builds, **golangci-lint**, **shellcheck**,<br/>unit tests and coverage,<br/>and the documentation build, preview, and checks. |
| The host           | **workshopd**, the **spread** suites,<br/>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.

<a id="contributing-dev-workshop"></a>

### Launch the dev workshop

Install **Workshop** and LXD first
(see [Install Workshop](https://ubuntu.com/workshop/docs//tutorial/part-1-get-started.md#tut-install)),
then launch the workshop from the repository root:

```console
$ 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](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-in-project-sdk)
in `.workshop/tools/`,
which the definition lists as `project-tools`;
the `project-` prefix selects the in-project source
(see [SDK entry](https://ubuntu.com/workshop/docs//reference/definition-files/workshop-definition.md#ref-workshop-definition-sdk-entry)).
Its [hooks](https://ubuntu.com/workshop/docs//explanation/sdks/runtime-hooks.md#exp-sdk-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](https://ubuntu.com/workshop/docs//explanation/sdks/runtime-hooks.md#exp-workshopctl-health)).
The SDK also slots a [tunnel](https://ubuntu.com/workshop/docs//explanation/interfaces/tunnel-interface.md#exp-tunnel-interface)
that the [system SDK](https://ubuntu.com/workshop/docs//explanation/sdks/concepts.md#exp-system-sdk) plugs
to publish the documentation preview
on the host at `127.0.0.1:8000`
(see [How to forward ports with tunneling](https://ubuntu.com/workshop/docs//how-to/customize-workshops/forward-ports.md#how-forward-ports)).

List the available actions and run one:

```console
$ workshop actions dev
$ workshop run dev lint
```

Because `dev` is the only definition in `.workshop/`,
you can omit its name.
See [workshop actions](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-actions) and [workshop run](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-run).

Open an interactive shell with [workshop shell](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-shell),
run a single command with [workshop exec](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-workshop-exec),
and apply edits to `.workshop/dev.yaml`
or to the SDK in `.workshop/tools/`
with [workshop refresh](https://ubuntu.com/workshop/docs//reference/cli/workshop.md#ref-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`:

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

To run **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
```

### Install Spread

[Spread](https://github.com/dmitry-lyfar/spread) is the end-to-end testing tool
for **Workshop**.
It launches its own LXD containers,
so install and run it on the host:

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

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

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

## Choose a task

Work is tracked in the project’s
[issue tracker](https://github.com/canonical/workshop/issues).
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](https://ubuntu.com/workshop/docs//coding-style-guide.md#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](#contributing-dev-workshop));
each check also has a host-native equivalent
if you have the tools installed.

**Workshop** tests use
[gocheck](https://pkg.go.dev/gopkg.in/check.v1#section-readme),
which integrates with **go test**.
Run the unit tests with coverage:

```console
$ workshop run dev cover
```

Or on the host:

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

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

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

To control the coverage scope and mode:

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

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

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

```console
$ go tool cover -html=coverage.out
```

Formatting and common pitfalls are checked with
[golangci-lint](https://golangci-lint.run/).
Lint the sources in both the full and the incremental configuration:

```console
$ workshop run dev lint
```

Or on the host:

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

Some issues can be fixed automatically:

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

Or on the host:

```console
$ golangci-lint run --fix
```

Check the shell scripts and the **spread** task definitions:

```console
$ workshop run dev shellcheck
```

If [pre-commit](https://pre-commit.com/index.html#install) is available,
**git** can run the linters on every commit
using the same pinned **golangci-lint**:

```console
$ pre-commit install
```

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

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

Run the end-to-end suites with
[Spread](https://github.com/canonical/spread), also on the host:

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

```console
$ 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](https://ubuntu.com/workshop/docs//contributing/documentation.md#contributing-documentation) for how to write, build, and test them.

### Commit

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

## Review with the team

### Send for review

Open a [pull request](https://github.com/canonical/workshop/pulls)
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](https://ubuntu.com/workshop/docs//contributing/maintenance.md#contributing-cicd).

After receiving review comments, optimize for commit history clarity.
Address them 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.

### 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](https://github.com/canonical/workshop/issues).
