diff --git a/README.md b/README.md index 472a7bb..041617b 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,9 @@ The demo_configuration folder in this repository can be passed as the config_fol the deploy-tools commands. The deployment_root needs to be a writeable location for all files to get deployed under. -In normal use these commands are not run by hand: they act on a shared deployment area and -belong in a CI pipeline, gated by change review. Running them manually against the demo -configuration, as below, is just the quickest way to see what each does — the +These commands act on a shared deployment area, so we recommend running them from a CI +pipeline gated by change review rather than by hand. Running them manually against the demo +configuration, as below, is the quickest way to see what each does — the [documentation](https://diamondlightsource.github.io/deploy-tools) has a hands-on tutorial and a guide to driving them from CI. @@ -46,7 +46,7 @@ deploy-tools validate $deployment_root $config_folder deploy-tools sync $deployment_root $config_folder # Compare the current deployment snapshot against what is actually deployed in the -# deployment area. CI/CD should run this before a deploy to confirm a healthy state. +# deployment area. Run this before a deploy to confirm a healthy state. deploy-tools compare $deployment_root ``` diff --git a/docs/conf.py b/docs/conf.py index 7d36536..5082de7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,6 +60,9 @@ # So we can use the ::: syntax myst_enable_extensions = ["colon_fence"] +# Generate anchors for headings (levels 1-3) so pages can link to sections. +myst_heading_anchors = 3 + # If true, Sphinx will warn about all references where the target cannot # be found. nitpicky = True diff --git a/docs/explanations/deployment-area.md b/docs/explanations/deployment-area.md index 0f11755..74be178 100644 --- a/docs/explanations/deployment-area.md +++ b/docs/explanations/deployment-area.md @@ -37,7 +37,8 @@ corresponding `modulefile`. Users put only the *modulefiles* directories on thei Deprecation is therefore cheap and reversible: the built files never move, only the symlink moves between `modulefiles/` and `deprecated/modulefiles/`. See -[the release lifecycle](deprecation-lifecycle.md) for the full set of transitions. +[the release lifecycle](deprecation-lifecycle.md#configuration-is-declarative) for +the full set of transitions. ## The build area diff --git a/docs/explanations/deployment-steps.md b/docs/explanations/deployment-steps.md index 349a5a4..07d9354 100644 --- a/docs/explanations/deployment-steps.md +++ b/docs/explanations/deployment-steps.md @@ -7,7 +7,7 @@ a command of their own. For the commands themselves see the [CLI reference](../c | Step | Description | Run by | |------|-------------|--------| | Compare | Compare the current deployment snapshot against the modulefiles and built modules that actually exist, confirming the [deployment area](deployment-area.md) is healthy. | `compare` | -| Validate | Diff the new configuration against the current snapshot to determine the set of actions to take, and check those actions are permitted by the [release lifecycle](deprecation-lifecycle.md). | `validate`, `sync` | +| Validate | Diff the new configuration against the current snapshot to determine the set of actions to take, and check those actions are permitted by the [lifecycle guard rails](deprecation-lifecycle.md#the-guard-rails). | `validate`, `sync` | | Build | Generate entrypoint scripts, configuration files and environment variables for each changed Module, writing them to the build area. | `sync` (`validate --test-build`) | | Deploy | Move the built Modules from the build area into the Modules Area, link each modulefile into the live or deprecated tree according to its status, and update default versions. | `sync` | @@ -23,4 +23,4 @@ place, or nothing changes. read-only checks run before it. See [snapshots and the compare safety net](snapshots-and-compare.md) for how the snapshot ties them together, and [drive deploy-tools from CI](../how-to/ci-pipeline.md) for the -order a pipeline runs them in. +order to run them in. diff --git a/docs/explanations/deprecation-lifecycle.md b/docs/explanations/deprecation-lifecycle.md index 39a6b95..b8a200e 100644 --- a/docs/explanations/deprecation-lifecycle.md +++ b/docs/explanations/deprecation-lifecycle.md @@ -9,8 +9,8 @@ deleted — is expressed by adding or removing a Release in configuration and to You never tell `deploy-tools` to "deprecate" or "remove" something directly. You describe the set of Releases you want, and the tool compares that against the -[snapshot](snapshots-and-compare.md) of the last `sync` to derive the actions needed. Each -Release falls into one of these cases: +[snapshot](snapshots-and-compare.md#what-the-snapshot-is) of the last `sync` to derive +the actions needed. Each Release falls into one of these cases: | Transition | Detected when… | Effect on the deployment area | |------------|----------------|-------------------------------| diff --git a/docs/explanations/snapshots-and-compare.md b/docs/explanations/snapshots-and-compare.md index 7c864e3..ddfbf5b 100644 --- a/docs/explanations/snapshots-and-compare.md +++ b/docs/explanations/snapshots-and-compare.md @@ -37,7 +37,7 @@ will not be detected. The risk of corruption is avoided at build time instead: a is built on the same filesystem as the deployment area and published by a single atomic rename, so a partial or failed build (including `.sif` files) is never moved into place. -This is why CI should run `compare` *before* every `sync`: it confirms the area is in the +This is why `compare` should be run *before* every `sync`: it confirms the area is in the healthy state the last `sync` claimed to leave it in. ## Recovery is manual @@ -55,7 +55,7 @@ Two facilities help here: often easier to rollback the configuration to a previous state rather than fix the latest deployment. - `compare --from-scratch` asserts only that the deployment root exists and is *empty* — - this is the check to run in CI before the very first `sync`, when no snapshot exists yet. + this is the check to run before the very first `sync`, when no snapshot exists yet. The git repository in the deployment area exists only to give `compare --use-ref` this reference point. It deliberately excludes the build area and Apptainer images, and is diff --git a/docs/glossary.md b/docs/glossary.md index e53d2cf..261a6a3 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -9,8 +9,9 @@ for the on-disk layout these terms refer to. | Environment Modules | A [standard package for Linux](https://modules.readthedocs.io/en/latest/) that provides the commands for loading and unloading 'Environment Modules' (each defined by a Modulefile). Note that while we are using this system, our definition of Module is separate. If we are referring to an Environment Module, we will use the full name. | | Modulefile | Used by the Environment Modules package to specify all details of an Environment Module. This can include executables to add to the path, environment variables to set, etc. | | Module | A set of files that can be used to provide applications on your path, provide configuration, and set environment variables. We do this using the Environment Modules system by providing a Modulefile with the relevant configuration. | -| Application | Each Module can be configured with multiple Applications, each one providing one or more executables. There are 3 types of Application: `Apptainer` (an executable container image), `Shell` (a Bash script) and `Binary` (an executable downloaded from a URL and verified against a hash). | +| Application | Each Module can be configured with multiple Applications, each one providing one or more executables. There are 3 types of Application: `Apptainer` (an executable container image), `Shell` (a Bash script) and `Binary` (an executable downloaded from a URL and verified against a hash). See [the configuration reference](reference/configuration.md). | | Release (noun) | A Module, including version, alongside its [lifecycle](explanations/deprecation-lifecycle.md) (i.e. deprecation) status. | +| Release file | The `/.yaml` file you author to define one Release. See [the configuration reference](reference/configuration.md). | | Deployment | The declared configuration for a Deployment Area: all Releases (deprecated or not) to be maintained there, plus global settings. Written to the `deployment.yaml` snapshot by `sync`. The act of deploying is written lowercase. | | Deployment Step | Refers to one of the primary steps that make up the deployment process. See [the deployment process](explanations/deployment-steps.md) for a breakdown. | | End User | Refers to anybody who is intending to make use of a deployed Module. This can include the people modifying configuration themselves. | diff --git a/docs/how-to.md b/docs/how-to.md index 649f7b0..c6149a6 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -5,6 +5,7 @@ Practical step-by-step guides for the more experienced user. ```{toctree} :maxdepth: 1 +how-to/write-module-configuration how-to/ci-pipeline how-to/run-container how-to/vscode-tasks diff --git a/docs/how-to/ci-pipeline.md b/docs/how-to/ci-pipeline.md index 55893b9..c7fd1e5 100644 --- a/docs/how-to/ci-pipeline.md +++ b/docs/how-to/ci-pipeline.md @@ -1,12 +1,15 @@ # Drive deploy-tools from CI -In normal use, nobody should run `sync`, `validate` or `compare` by hand. Those commands -touch a shared deployment area, so they belong to a CI pipeline in the configuration -repository, gated by change review. End users only edit configuration and open a change; -merging it deploys. +`sync`, `validate` and `compare` touch a shared deployment area, so we recommend driving +them from a CI pipeline in the configuration repository, gated by change review: end users +only edit configuration and open a change, and merging it deploys. `deploy-tools` does not +require this — an administrator can run the commands by hand — but a pipeline gives you +review, repeatability and one place to serialise runs. -This guide describes what that pipeline must do. It is deliberately independent of any -particular CI system — translate the responsibilities below into your own. +This guide describes the responsibilities such a pipeline has. It is deliberately +independent of any particular CI system — translate them into your own. If you run the +commands by hand, the same ordering and the [one at a time](#run-one-at-a-time) rule still +apply. ## On a proposed change @@ -15,9 +18,9 @@ When someone opens a change (before it is merged), run, without altering the are - `deploy-tools compare ` — confirm the area still matches its last snapshot, so the change is being checked against a known-healthy baseline. - `deploy-tools validate ` — confirm the new configuration is valid and its - [lifecycle transitions](../explanations/deprecation-lifecycle.md) are permitted. Add - `--test-build` to build every changed Module in a temporary directory, catching build - failures before merge. + [lifecycle transitions](../explanations/deprecation-lifecycle.md#the-guard-rails) are + permitted. Add `--test-build` to build every changed Module in a temporary directory, + catching build failures before merge. This gives reviewers a green light that the change is deployable without changing anything on the filesystem. @@ -32,15 +35,15 @@ When the change is merged to the main branch, deploy it: ## Run one at a time -`deploy-tools` has no locking of its own. The pipeline must ensure only one run +`deploy-tools` has no locking of its own. Whatever drives it must ensure only one run touches the area at a time — two concurrent `sync`s, or a `sync` racing a `compare`, can corrupt the area or report false drift. Serialise the relevant jobs (and, if possible, restrict them to a single runner). ## Manual operations -Some tasks fall outside the automatic flows and are best run from a manually-triggered -pipeline, exposing the relevant options as parameters: +Some tasks fall outside the automatic flows and are run by hand, or from a +manually-triggered pipeline exposing the relevant options as parameters: - **The first deployment.** A brand-new area has no snapshot to compare against, so it is run manually rather than triggered by a merge. Use `--from-scratch`, which assumes the diff --git a/docs/how-to/regenerate-schemas.md b/docs/how-to/regenerate-schemas.md index f707561..9d67888 100644 --- a/docs/how-to/regenerate-schemas.md +++ b/docs/how-to/regenerate-schemas.md @@ -12,7 +12,7 @@ schemas and commit again. If you bypass the hooks (for example with `git commit --no-verify`), regenerate the schemas manually. The simplest way is the **Generate Schema** VSCode task (see -[the VSCode tasks guide](vscode-tasks.md)), which writes to the correct location. +[the VSCode tasks guide](vscode-tasks.md#running-a-task)), which writes to the correct location. Equivalently, run the CLI, pointing it at that folder: ```bash diff --git a/docs/how-to/run-container.md b/docs/how-to/run-container.md index bb1ec23..a1b8a7b 100644 --- a/docs/how-to/run-container.md +++ b/docs/how-to/run-container.md @@ -12,3 +12,6 @@ $ docker run ghcr.io/diamondlightsource/deploy-tools:latest --version ``` To get a released version, use a numbered release instead of `latest`. + +This image is a convenient way to drive `deploy-tools` against a shared deployment area, +typically from a CI pipeline — see [drive deploy-tools from CI](ci-pipeline.md). diff --git a/docs/how-to/write-module-configuration.md b/docs/how-to/write-module-configuration.md new file mode 100644 index 0000000..5063119 --- /dev/null +++ b/docs/how-to/write-module-configuration.md @@ -0,0 +1,88 @@ +# Write a Module configuration + +To add or change a Module, you edit YAML in the configuration folder. This guide covers +the mechanics. For the shape of the files and every field they take, see +[the configuration reference](../reference/configuration.md); to point your editor at +the matching schema, the [schema reference](../schemas.md). + +## Add a new Module version + +1. Create the Release file at `//.yaml`. The folder name + is the Module `name` and the filename is the `version`, so `phoebus/0.1.yaml` defines + version `0.1` of `phoebus`. The `name` and `version` inside the file must match the + path. + +2. Add the schema line as the first line so your editor validates as you type: + + ```yaml + # yaml-language-server: $schema=https://raw.githubusercontent.com/DiamondLightSource/deploy-tools/main/src/deploy_tools/models/schemas/release.json + ``` + + The line is read by [`yaml-language-server`](https://github.com/redhat-developer/yaml-language-server), + so it takes effect in VS Code with the Red Hat YAML extension or any other editor + running that language server; elsewhere it is an inert comment. See the + [schema reference](../schemas.md) for details. + +3. Define the Module. Most Modules provide one or more applications; the smallest useful + one is a single shell script: + + ```yaml + module: + name: my-module + version: "1.0" + description: What this Module provides + applications: + - app_type: shell + name: hello + script: + - echo "hello from my-module" + ``` + + Swap the application for an `apptainer` or `binary` one as needed — see + [the three application types](../reference/configuration.md#the-three-application-types). + + A Module doesn't have to provide an application: it can instead just set environment + variables or pull in other Modules as + [dependencies](../reference/configuration.md#a-module). Give such a Module an + empty `applications: []`. + +## Set the default version + +`module load ` with no version loads the default. If you don't choose one the +highest version is picked automatically; to pin a specific version, add it to +`settings.yaml`: + +```yaml +default_versions: + my-module: "1.0" +``` + +`settings.yaml` can take a schema line of its own, as +[above](#add-a-new-module-version), pointing at `deployment-settings.json`. + +To keep a version out of automatic selection — an alpha or release candidate, say — while +still allowing an explicit `module load /`, set +`exclude_from_defaults: true` on that Module. + +See [default version resolution](../explanations/default-versions.md) for how the +automatic choice is made. + +## Get your change deployed + +How your change reaches the deployment area depends on how your site runs `deploy-tools`. +The recommended setup is a CI pipeline in the configuration repository: you open a merge +request, CI validates the change, and merging it deploys (see +[drive deploy-tools from CI](ci-pipeline.md)). CI is not a requirement — an administrator +can run the same `validate` and `sync` commands by hand instead. Either way, the +`yaml-language-server` schema line catches structural mistakes in your editor as you type, +before anyone else looks at the change. + +## Change or retire a version + +- **Update an existing version in place.** Rejected by default so published versions stay + stable; prefer publishing a new version. If you must, set `allow_updates: true` on the + Module — see [the guard rails](../explanations/deprecation-lifecycle.md#the-guard-rails). +- **Retire a version.** Set `deprecated: true` in the Release file to steer users away + from it. Deleting it outright has to wait until after it is deprecated (unless the + Module has `allow_updates: true`). See + [the guard rails](../explanations/deprecation-lifecycle.md#the-guard-rails). diff --git a/docs/reference.md b/docs/reference.md index da6115c..b26e7b1 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -6,6 +6,7 @@ Technical reference material including APIs and release notes. :maxdepth: 1 :glob: +reference/configuration CLI API <_api/deploy_tools> Schemas diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md new file mode 100644 index 0000000..c2571ac --- /dev/null +++ b/docs/reference/configuration.md @@ -0,0 +1,175 @@ +# Configuration + +This page is for anyone writing or editing deployment configuration. It documents every +field of the files you author and how they nest. The field tables are curated by hand +from the Pydantic models in `src/deploy_tools/models`, so a change to those models needs +a change here too; the [schema reference](../schemas.md) renders the same fields +mechanically from the generated JSON schemas. For the meaning of individual terms, see +the [glossary](../glossary.md). + +## What you author + +You author two kinds of file: a single `settings.yaml`, and one Release file, +`/.yaml`, per Module version. The [schema reference](../schemas.md) covers +which schema validates which and how to point your editor at them. + +They are structured as follows: + +```text +settings.yaml + └── default_versions + +/.yaml one Release file per Module version + ├── deprecated: false lifecycle flag + └── module + ├── name Module name + ├── version Module version + ├── description shown by `module whatis` + ├── env_vars variables set on load + ├── dependencies other Modules to load first + └── applications one or more, each with an app_type of: + ├── apptainer container image + entrypoints + ├── shell a bash script + └── binary a downloaded, hash-checked executable +``` + +Those last three are the values `app_type` can take rather than fields of their own — +see [the three application types](#the-three-application-types) below. + +## A Release file + +Each Release file has two fields: + +| Field | Purpose | +|-------|---------| +| `module` | The Module being released (below). | +| `deprecated` | Whether this version is deprecated. Defaults to `false` — see [the release lifecycle](../explanations/deprecation-lifecycle.md). | + +## A Module + +A Module is the unit an end user loads. It carries: + +| Field | Purpose | +|-------|---------| +| `name` | The name an end user loads it by. | +| `version` | The version an end user loads it by. | +| `description` | Shown by `module whatis `. | +| `env_vars` | Environment variables set when the Module is loaded. | +| `dependencies` | Other Modules loaded first, optionally version-pinned. | +| `applications` | One or more applications providing the executables (below). | +| `load_script` | Extra commands run when the Module is loaded — see below. | +| `unload_script` | Extra commands run when the Module is unloaded — see below. | +| `allow_updates` | Permit in-place changes to this version — see [the guard rails](../explanations/deprecation-lifecycle.md#the-guard-rails). | +| `exclude_from_defaults` | Keep this version out of automatic default selection — see [default versions](../explanations/default-versions.md#excluding-a-version-from-the-automatic-default). | + +**`env_vars`** — each entry is a name/value pair: + +| Field | Purpose | +|-------|---------| +| `name` | The variable to set. | +| `value` | The value to set it to. | + +**`dependencies`** — each entry names another Module: + +| Field | Purpose | +|-------|---------| +| `name` | The Module to load first. | +| `version` | The version to pin to. If omitted, that Module's default version is resolved at load time. | + +`load_script` and `unload_script` are injected raw into the generated Modulefile. They +are for advanced cases the other fields cannot cover — check with a `deploy-tools` admin +before using them. + +## The three application types + +Every entry under `applications` sets `app_type` to select one of three kinds; a single +Module can mix them. + +| `app_type` | Provides | +|------------|----------| +| `apptainer` | Commands that run inside a container image | +| `shell` | A single executable running a bash script | +| `binary` | A downloaded executable added to the path | + +The demo `example-module-apps` Module combines an Apptainer app with a Shell app: + +```{literalinclude} ../../src/deploy_tools/demo_configuration/example-module-apps/0.1.yaml +:language: yaml +:lines: 3- +``` + +### Apptainer + +One container image with one or more entrypoints, each mapping an executable name to a +command run inside the container. + +| Field | Purpose | +|-------|---------| +| `container` | The image to use (below). | +| `entrypoints` | The executables provided (below). | +| `global_options` | Options applied to every entrypoint. | + +**`container`** — splits the image reference into `path:version`: + +| Field | Purpose | +|-------|---------| +| `path` | The image URL, excluding the version or tag. `docker`, `shub`, `oras` and `https` schemes are accepted. | +| `version` | The image version or tag. | + +**`entrypoints`** — each entry is one executable: + +| Field | Purpose | +|-------|---------| +| `name` | The executable provided. | +| `command` | The command to run inside the container. Defaults to `name`. | +| `options` | Options applied to this entrypoint only. | + +**`options` and `global_options`** — both take the same fields: + +| Field | Purpose | +|-------|---------| +| `apptainer_args` | Arguments passed to Apptainer when launching the container. | +| `command_args` | Arguments passed to the command being run. | +| `mounts` | Mount points as `host_path[:container_path[:opts]]`, where `opts` is `ro` or `rw` (default `rw`). | +| `host_binaries` | Host binaries, found on the current `PATH`, to mount into the container at `/usr/bin/`. | + +### Shell + +A single executable running a bash script. + +| Field | Purpose | +|-------|---------| +| `name` | The executable provided. | +| `script` | The lines of bash it runs. | + +### Binary + +An executable downloaded, hash-checked and added to the path. + +| Field | Purpose | +|-------|---------| +| `name` | The executable provided. | +| `url` | Where the binary is downloaded from. | +| `hash` | The expected hash of the download. | +| `hash_type` | `sha256`, `sha512`, `md5`, or `none` to skip the check. | + +The demo `argocd` Module uses one: + +```{literalinclude} ../../src/deploy_tools/demo_configuration/argocd/v2.14.10.yaml +:language: yaml +:lines: 3- +``` + +## Settings + +`settings.yaml` holds deployment-wide settings. It currently has a single field, +`default_versions`, mapping a Module name to the version handed to `module load ` +when no version is given: + +```{literalinclude} ../../src/deploy_tools/demo_configuration/settings.yaml +:language: yaml +:lines: 3- +``` + +How that choice is resolved — and how to keep a version out of automatic selection — is +covered in [default version resolution](../explanations/default-versions.md). diff --git a/docs/schemas.md b/docs/schemas.md index 05893d6..0cf9a9d 100644 --- a/docs/schemas.md +++ b/docs/schemas.md @@ -2,8 +2,13 @@ These JSON schema files are generated from the Pydantic models in `src/deploy_tools/models`. The YAML language server and other tooling use them to validate -deployment configuration files. This section links to per-schema reference pages that -render each schema and list its properties. +deployment configuration files. This page covers which file each schema validates and how +to point your editor at it. + +To look a field up, use [the configuration reference](reference/configuration.md) — it +documents the same fields in a form meant for reading. The +[schema pages](#schema-pages) below render each schema mechanically, so they are the +exhaustive detail rather than the place to start. ## Which file uses which schema @@ -14,7 +19,7 @@ You author two kinds of configuration file, each validated against a different s | `settings.yaml` (one per config folder) | `deployment-settings.json` | `DeploymentSettings` | | `/.yaml` (one per Module version) | `release.json` | `Release` | -Per-version files sit in a folder named after the Module, so the path is +Release files sit in a folder named after the Module, so the path is `//.yaml` (folder = Module `name`, filename = `version`). Add a `yaml-language-server` comment as the first line of each file, pointing at the matching schema, so your editor validates it as you type: @@ -28,6 +33,12 @@ This requires an editor with a YAML language server — e.g. VS Code with the or any [LSP](https://microsoft.github.io/language-server-protocol/)-capable editor running [`yaml-language-server`](https://github.com/redhat-developer/yaml-language-server). +Alternatively, VS Code's [`yaml.schemas`](https://github.com/redhat-developer/vscode-yaml#associating-schemas) +setting (committed to `.vscode/settings.json`) maps schemas to file paths once for the +whole repository, avoiding the per-file comment. This documentation uses the comment +because it is self-contained and editor-agnostic, but a real configuration repository may +reasonably prefer the repository-level setting. + ```{note} The bundled `demo_configuration` instead points at the locally generated schemas via an absolute workspace path (e.g. diff --git a/docs/tutorials/your-first-deployment.md b/docs/tutorials/your-first-deployment.md index 18c360e..a502cdc 100644 --- a/docs/tutorials/your-first-deployment.md +++ b/docs/tutorials/your-first-deployment.md @@ -4,9 +4,10 @@ This tutorial takes you from a set of configuration files to a Module you can lo using the demo configuration that ships in the repository. It is a hands-on learning exercise: you run each command yourself, in a throwaway directory. -Real deployments don't work this way — a CI pipeline runs these commands against a shared -area (see [drive deploy-tools from CI](../how-to/ci-pipeline.md)) — but running them by -hand once is the quickest way to see what each does. +A real deployment targets a shared area, where we recommend driving these commands from +a CI pipeline instead (see +[drive deploy-tools from CI](../how-to/ci-pipeline.md)) — but running them by hand once +is the quickest way to see what each does. ## Before you start @@ -27,9 +28,9 @@ $ git clone https://github.com/DiamondLightSource/deploy-tools.git $ export CONFIG=$PWD/deploy-tools/src/deploy_tools/demo_configuration ``` -That folder contains a `settings.yaml` and one folder per Module, each holding a -`.yaml` file. Have a look — `example-module-apps/0.1.yaml`, for example, defines -a Module with a containerised app and a couple of small shell entrypoints. +That folder contains a `settings.yaml` and one folder per Module, each holding a Release +file named `.yaml`. Have a look — `example-module-apps/0.1.yaml`, for example, +defines a Module with a containerised app and a couple of small shell entrypoints. ```{note} The `# yaml-language-server: $schema=…` line at the top of each file only drives editor @@ -145,8 +146,8 @@ dls-pmac-control` loads `0.1` unless you ask for `dls-pmac-control/0.2` explicit You took a configuration folder, checked the area was clean, previewed the changes, and deployed Modules you could load and run. Those are the same `compare`, `validate` and -`sync` commands a real pipeline runs, though it spreads them across separate stages (see -[drive deploy-tools from CI](../how-to/ci-pipeline.md)). +`sync` commands a real deployment runs; our recommended pipeline spreads them across +separate stages (see [drive deploy-tools from CI](../how-to/ci-pipeline.md)). From here: