diff --git a/.github/actions/determine-release-modes/README.md b/.github/actions/determine-release-modes/README.md index 9085c0c3..c0d8cbe2 100644 --- a/.github/actions/determine-release-modes/README.md +++ b/.github/actions/determine-release-modes/README.md @@ -10,16 +10,16 @@ type and optional input overrides. | Name | Description | Required | Default | | ---- | ----------- | -------- | ------- | -| `dry-run` | Override dry-run mode (auto-detected from event when empty) | no | `""` | -| `publish` | Override publish flag (auto-detected from event when empty) | no | `""` | +| `dry-run` | Override dry-run (auto-detected) | no | `""` | +| `publish` | Override publish flag (auto-detected) | no | `""` | ## Outputs | Name | Description | | ---- | ----------- | -| `dry-run` | `"true"` or `"false"` indicating dry-run mode | -| `should-publish` | `"true"` or `"false"` indicating whether to publish to a release | -| `should-upload-workflow-artifacts` | `"true"` or `"false"` indicating whether to upload workflow artefacts | +| `dry-run` | Dry-run (`"true"` or `"false"`) | +| `should-publish` | Publish (`"true"` or `"false"`) | +| `should-upload-workflow-artifacts` | Upload (`"true"` or `"false"`) | ## Usage @@ -56,7 +56,7 @@ type and optional input overrides. The action derives modes based on the GitHub event type: | Event | Default dry-run | Default publish | Artefacts | -|-------|-----------------|-----------------|-----------| +| --- | --- | --- | --- | | `push` (tag) | `false` | `true` | Uploaded | | `workflow_call` | From inputs | From inputs | If not dry-run | | `pull_request` | `true` | `false` | None | diff --git a/.github/actions/ensure-cargo-version/README.md b/.github/actions/ensure-cargo-version/README.md index 824f907d..88c7fbb2 100644 --- a/.github/actions/ensure-cargo-version/README.md +++ b/.github/actions/ensure-cargo-version/README.md @@ -1,22 +1,23 @@ # ensure-cargo-version -Validate that the Git tag triggering a release workflow matches the version in one or more Cargo manifests. +Validate that the Git tag triggering a release workflow matches the version in +one or more Cargo manifests. ## Inputs | Name | Required | Default | Description | | ---- | -------- | ------- | ----------- | -| `manifests` | No | `Cargo.toml` | Newline or whitespace separated list of Cargo manifest paths to check. Paths are resolved relative to the GitHub workspace. | -| `tag-prefix` | No | `v` | Prefix stripped from the Git reference name before comparing against manifest versions. Use an empty string to disable prefix removal. | -| `check-tag` | No | `true` | Disable tag comparison by supplying a falsey value (case-insensitive `false`, `0`, `no`, `off`, or an empty string). Truthy values (`true`, `1`, `yes`, `on`) enable comparison while still attempting to read the tag for output purposes. | +| `manifests` | No | `Cargo.toml` | Manifest paths (newline separated) | +| `tag-prefix` | No | `v` | Git tag prefix to strip | +| `check-tag` | No | `true` | Enable tag comparison | ## Outputs | Name | Description | | ---- | ----------- | -| `version` | Version extracted from the tag reference after removing the configured prefix. | -| `crate-version` | Version read from the first manifest path provided (after resolution) after resolving workspace inheritance. | -| `crate-name` | Package name read from the first manifest path provided (after resolution). | +| `version` | Version from tag (prefix removed) | +| `crate-version` | Version from first manifest | +| `crate-name` | Package name from first manifest path | ## Usage diff --git a/.github/actions/export-cargo-metadata/README.md b/.github/actions/export-cargo-metadata/README.md index 5aa3ee46..05f7fccb 100644 --- a/.github/actions/export-cargo-metadata/README.md +++ b/.github/actions/export-cargo-metadata/README.md @@ -15,10 +15,10 @@ manifest and export as GitHub Actions outputs and environment variables. | Name | Description | | ---- | ----------- | -| `name` | Package name from `[package].name` | -| `version` | Package version from `[package].version` (resolves workspace inheritance) | -| `bin-name` | Binary name from first `[[bin]].name` or `[package].name` | -| `description` | Package description from `[package].description` | +| `name` | Package name | +| `version` | Package version | +| `bin-name` | Binary name | +| `description` | Package description | ## Usage diff --git a/.github/actions/linux-packages/README.md b/.github/actions/linux-packages/README.md index e2419355..8f72c656 100644 --- a/.github/actions/linux-packages/README.md +++ b/.github/actions/linux-packages/README.md @@ -11,27 +11,27 @@ nFPM. | Name | Type | Default | Description | Required | | ---- | ---- | ------- | ----------- | -------- | -| project-dir | string | `.` | Directory containing the compiled binary, man pages and optional license file. | no | -| package-name | string | _empty_ | Package identifier written to the nFPM manifest. Defaults to `bin-name` when omitted. | no | -| bin-name | string | — | Name of the release binary to package. | yes | -| target | string | `x86_64-unknown-linux-gnu` | Rust target triple used for the build. | no | -| version | string | — | Version number recorded in the package metadata (for example `1.2.3`). | yes | -| formats | string | `deb` | Comma-, space-, or newline-separated list of package formats (for example `deb,rpm` or a multi-line value). | no | -| release | string | _empty_ | Package release or revision override. Uses the packaging helper default when omitted. | no | -| arch | string | _empty_ | Override the nFPM/GOARCH architecture. Auto-detected from `target` when not set. | no | -| maintainer | string | _empty_ | Maintainer entry for the generated package metadata. | no | -| homepage | string | _empty_ | Homepage URL recorded in package metadata. | no | -| license | string | _empty_ | Software license declared in the package metadata. | no | -| section | string | _empty_ | Package section/category used by Debian-based distributions. | no | -| description | string | _empty_ | Long description stored in the package metadata. | no | -| man-paths | string | _empty_ | Comma-, space-, or newline-separated list of man page paths relative to `project-dir`. | no | -| man-section | string | _empty_ | Default man section applied when a path lacks a suffix (for example `1`). | no | -| man-stage | string | _empty_ | Directory used to stage gzipped man pages before invoking nFPM. | no | -| binary-dir | string | _empty_ | Cargo `target` directory containing build artefacts. | no | -| outdir | string | _empty_ | Directory where packages will be written. | no | -| config-path | string | _empty_ | Location to write the generated `nfpm.yaml` configuration. | no | -| deb-depends | string | _empty_ | Comma-, space-, or newline-separated Debian runtime dependencies (each entry becomes a separate dependency in the generated manifest). | no | -| rpm-depends | string | _empty_ | Comma-, space-, or newline-separated RPM runtime dependencies. Falls back to Debian deps when omitted. | no | +| project-dir | string | `.` | Binary, man, license dir | no | +| package-name | string | _empty_ | Package ID (defaults to bin) | no | +| bin-name | string | — | Binary name | yes | +| target | string | `x86_64-unknown-linux-gnu` | Rust target triple | no | +| version | string | — | Package version | yes | +| formats | string | `deb` | Formats (e.g. `deb,rpm`) | no | +| release | string | _empty_ | Release/revision override | no | +| arch | string | _empty_ | Architecture (auto-detected) | no | +| maintainer | string | _empty_ | Package maintainer | no | +| homepage | string | _empty_ | Homepage URL | no | +| license | string | _empty_ | Software license | no | +| section | string | _empty_ | Package section | no | +| description | string | _empty_ | Package description | no | +| man-paths | string | _empty_ | Man page paths | no | +| man-section | string | _empty_ | Default man section | no | +| man-stage | string | _empty_ | Man staging directory | no | +| binary-dir | string | _empty_ | Cargo target directory | no | +| outdir | string | _empty_ | Output directory | no | +| config-path | string | _empty_ | nfpm config path | no | +| deb-depends | string | _empty_ | Debian dependencies | no | +| rpm-depends | string | _empty_ | RPM dependencies | no | Before invoking sibling actions the composite mirrors the repository snapshot that GitHub already downloaded for the action into a local `_self/` directory. diff --git a/.github/actions/macos-package/README.md b/.github/actions/macos-package/README.md index 62b26e5e..14a6ccfd 100644 --- a/.github/actions/macos-package/README.md +++ b/.github/actions/macos-package/README.md @@ -12,24 +12,24 @@ executing any packaging commands. | Name | Description | Required | Default | | ---- | ----------- | -------- | ------- | -| `name` | Display name of the packaged CLI (also used for output filenames). | yes | – | -| `identifier` | Reverse-DNS package identifier (for example `com.example.tool`). | yes | – | -| `install-prefix` | Installation prefix within the target filesystem. | no | `/usr/local` | -| `binary` | Path to the compiled binary to install. | yes | – | -| `manpage` | Optional path to a man page (`.1`, `.1.gz`, etc.) to embed. | no | empty | -| `license-file` | Path to the license text copied into the package and optional UI. | no | `LICENSE` | -| `include-license-panel` | Show the license text inside the installer UI (requires `license-file`). | no | `false` | -| `version` | Override the package version. Defaults to the Git tag (`v*`) or falls back to the commit SHA. | no | derived | -| `developer-id-installer` | Developer ID Installer identity used with `productsign` for notarized packages. | no | empty | +| `name` | Display name | yes | – | +| `identifier` | Reverse-DNS identifier | yes | – | +| `install-prefix` | Install prefix | no | `/usr/local` | +| `binary` | Binary path | yes | – | +| `manpage` | Man page path | no | empty | +| `license-file` | License file path | no | `LICENSE` | +| `include-license-panel` | Show license in UI | no | `false` | +| `version` | Version override | no | derived | +| `developer-id-installer` | Developer ID identity | no | empty | ## Outputs | Name | Description | | ---- | ----------- | -| `version` | Resolved version used for the package metadata. | -| `version-build-metadata` | Short commit SHA recorded when falling back to a default version. | -| `pkg-path` | Path to the generated installer archive (`dist/-.pkg`). | -| `signed-pkg-path` | Path to the signed installer archive when signing succeeds; empty otherwise. | +| `version` | Resolved package version | +| `version-build-metadata` | Commit SHA (fallback) | +| `pkg-path` | Installer package path | +| `signed-pkg-path` | Signed package path | ## Usage diff --git a/.github/actions/ratchet-coverage/README.md b/.github/actions/ratchet-coverage/README.md index 1f574c4f..2aecb626 100644 --- a/.github/actions/ratchet-coverage/README.md +++ b/.github/actions/ratchet-coverage/README.md @@ -5,10 +5,10 @@ coverage percentage falls below a stored baseline. ## Inputs -| Name | Description | Required | Default | -| ------------- | ------------------------------------------------------------------ | -------- | -------------------- | -| baseline-file | File used to persist the baseline coverage percentage between runs | no | `.coverage-baseline` | -| args | Additional arguments passed to `cargo llvm-cov` | no | `''` | +| Name | Description | Required | Default | +| --- | --- | --- | --- | +| baseline-file | Baseline coverage file | no | `.coverage-baseline` | +| args | Additional cargo llvm-cov args | no | `''` | ## Outputs diff --git a/.github/actions/release-to-pypi-uv/README.md b/.github/actions/release-to-pypi-uv/README.md index 15bf5f6a..1ff186e0 100644 --- a/.github/actions/release-to-pypi-uv/README.md +++ b/.github/actions/release-to-pypi-uv/README.md @@ -7,16 +7,16 @@ Build and publish Python distributions via | Name | Description | Required | Default | | --- | --- | --- | --- | -| tag | Tag to release (e.g. `v1.2.3`). Required when the workflow is not running on a tag ref. | no | _(empty)_ | -| require-confirmation | Require a manual confirmation string before publishing. | no | `false` | -| confirm | Confirmation string. Must equal `release ` when `require-confirmation` is true. | no | _(empty)_ | -| environment-name | GitHub environment to reference in the release summary. | no | `pypi` | -| uv-index | Optional uv index name to publish to (e.g. `testpypi`). Must exist in `tool.uv.index`. | no | _(empty)_ | -| toml-glob | Glob used to discover `pyproject.toml` files for version validation. | no | `**/pyproject.toml` | -| skip-directories | Comma- or newline-separated directory names to skip during discovery. | no | _(empty)_ | -| fail-on-dynamic-version | Fail when a project declares a dynamic PEP 621 version instead of a literal string. | no | `false` | -| fail-on-empty | Fail when no `pyproject.toml` files match the discovery glob. | no | `false` | -| python-version | Python version to install and use for all uv commands. | no | `3.13` | +| tag | Tag to release (e.g. `v1.2.3`) | no | _(empty)_ | +| require-confirmation | Require manual confirmation | no | `false` | +| confirm | Confirmation string | no | _(empty)_ | +| environment-name | GitHub environment name | no | `pypi` | +| uv-index | uv index name (e.g. `testpypi`) | no | _(empty)_ | +| toml-glob | Glob for `pyproject.toml` discovery | no | `**/pyproject.toml` | +| skip-directories | Directories to skip | no | _(empty)_ | +| fail-on-dynamic-version | Fail on dynamic PEP 621 version | no | `false` | +| fail-on-empty | Fail on empty discovery | no | `false` | +| python-version | Python version | no | `3.13` | The composite action installs the interpreter requested through `python-version` before invoking any uv commands, ensuring builds run against the expected @@ -36,8 +36,11 @@ uses additional transient paths that should be excluded. | tag | Resolved release tag. | | version | Resolved release version (tag without the leading `v`). | -> **Required permissions**: set the job to `permissions: contents: read` and `permissions: id-token: write` so uv Trusted Publishing can exchange an OpenID Connect (OIDC) token with PyPI. -> The composite action forwards the workflow's `GITHUB_TOKEN` to its scripts as `GH_TOKEN`, so you do not need to add an extra `env` block. +> **Required permissions**: set the job to `permissions: contents: read` and +> `permissions: id-token: write` so uv Trusted Publishing can exchange an +> OpenID Connect (OIDC) token with PyPI. +> The composite action forwards the workflow's `GITHUB_TOKEN` to its scripts +> as `GH_TOKEN`, so you do not need to add an extra `env` block. ## Usage diff --git a/.github/actions/rust-build-release/CHANGELOG.md b/.github/actions/rust-build-release/CHANGELOG.md index c8f26447..37b7b707 100644 --- a/.github/actions/rust-build-release/CHANGELOG.md +++ b/.github/actions/rust-build-release/CHANGELOG.md @@ -11,17 +11,24 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added - Cross-compile and stage `x86_64-unknown-illumos` artefacts from Linux runners. -- Provide shared packaging fixtures and helpers that build the sample project once and produce `.deb` and `.rpm` artefacts for the integration tests. -- Support staging and packaging for `unknown-linux-musl` targets alongside GNU triples for x86_64, aarch64, i686, arm*, and riscv64 builds. -- Require containerized `cross` builds for FreeBSD targets on non-FreeBSD hosts to enable `x86_64-unknown-freebsd` cross-compilation. -- Automatically export `CROSS_CONTAINER_ENGINE` for the detected container runtime when running FreeBSD builds with `cross`. +- Provide shared packaging fixtures and helpers that build the sample project + once and produce `.deb` and `.rpm` artefacts for the integration tests. +- Support staging and packaging for `unknown-linux-musl` targets alongside GNU + triples for x86_64, aarch64, i686, arm*, and riscv64 builds. +- Require containerized `cross` builds for FreeBSD targets on non-FreeBSD hosts + to enable `x86_64-unknown-freebsd` cross-compilation. +- Automatically export `CROSS_CONTAINER_ENGINE` for the detected container + runtime when running FreeBSD builds with `cross`. - Add a `manifest-path` input for selecting an alternate Cargo manifest. - Add a `toolchain` input for explicitly overriding the resolved build toolchain. ### Fixed -- Pin `setup-rust` to the commit behind `setup-rust-v1`, so toolchain inputs and OS guards apply when invoked from external repositories. -- Resolve toolchains from the target repository before falling back to the action's bundled default: explicit input first, then `rust-toolchain.toml` or `rust-toolchain`, then manifest `rust-version`. +- Pin `setup-rust` to the commit behind `setup-rust-v1`, so toolchain inputs + and OS guards apply when invoked from external repositories. +- Resolve toolchains from the target repository before falling back to the + action's bundled default: explicit input first, then `rust-toolchain.toml` + or `rust-toolchain`, then manifest `rust-version`. ## [0.1.0] - 2025-09-10 diff --git a/.github/actions/upload-codescene-coverage/README.md b/.github/actions/upload-codescene-coverage/README.md index ba117241..c313f0c2 100644 --- a/.github/actions/upload-codescene-coverage/README.md +++ b/.github/actions/upload-codescene-coverage/README.md @@ -4,12 +4,12 @@ Upload coverage reports to CodeScene and cache the CLI for faster runs. ## Inputs -| Name | Description | Required | Default | -| ------------------ | ------------------------------------------------------------ | -------- | ----------- | -| path | Coverage file path; blank or `__auto__` infers automatically | no | `__auto__` | -| format | Coverage format (`cobertura` or `lcov`) | no | `cobertura` | -| access-token | CodeScene project access token | yes | | -| installer-checksum | SHA-256 checksum of the installer script | no | | +|Name|Description|Required|Default| +|---|---|---|---| +|path|Coverage file path; blank or `__auto__` infers automatically|no|`__auto__`| +|format|Coverage format (`cobertura` or `lcov`)|no|`cobertura`| +|access-token|CodeScene project access token|yes|| +|installer-checksum|SHA-256 checksum of the installer script|no|| If `path` is empty or `__auto__`, the action looks for `lcov.info` when `format` is `lcov`, or `coverage.xml` when `format` is `cobertura`. The diff --git a/.github/actions/upload-release-assets/README.md b/.github/actions/upload-release-assets/README.md index 1b24289e..135fa151 100644 --- a/.github/actions/upload-release-assets/README.md +++ b/.github/actions/upload-release-assets/README.md @@ -14,7 +14,7 @@ release state. | `release-tag` | Git tag identifying the release to publish to | yes | - | | `bin-name` | Binary name used to derive artefact names | yes | - | | `dist-dir` | Directory containing staged artefacts | no | `dist` | -| `dry-run` | When true, only validate artefacts and print the upload plan | no | `"false"` | +| `dry-run` | Validate artefacts without uploading | no | `"false"` | | `clobber` | Overwrite existing assets with the same name | no | `"true"` | ## Outputs @@ -90,7 +90,8 @@ Files in nested directories are namespaced with their path prefix, replacing 1. **Discovery**: Recursively scan `dist-dir` for matching artefacts 2. **Validation**: Verify files are non-empty and have unique asset names -3. **Upload**: Use `gh release upload` to publish artefacts (or print plan in dry-run mode) +3. **Upload**: Use `gh release upload` to publish artefacts (or print plan in + dry-run mode) ### Error Handling diff --git a/.github/actions/windows-package/README.md b/.github/actions/windows-package/README.md index f6076ad9..420db5f2 100644 --- a/.github/actions/windows-package/README.md +++ b/.github/actions/windows-package/README.md @@ -9,8 +9,9 @@ one composite call. - Installs the WiX CLI and UI extension on the runner. - Resolves the MSI version from an explicit input or a tagged Git reference. -- Builds a single-file MSI (`EmbedCab="yes"`) from supplied WiX authoring or a generated template that - installs the provided application and supporting files. +- Builds a single-file MSI (`EmbedCab="yes"`) from supplied WiX authoring or a + generated template that installs the provided application and supporting + files. - Optionally uploads the generated MSI via `actions/upload-artifact`. > [!IMPORTANT] @@ -32,37 +33,34 @@ inputs): └─ README.pdf # documentation shipped with the installer ``` -The `installer/Package.wxs` authoring is optional—omit it when using the default template and -provide the executable (and optional additional files) via the `application-path` and -`additional-files` inputs. +The `installer/Package.wxs` authoring is optional—omit it when using the +default template and provide the executable (and optional additional files) via +the `application-path` and `additional-files` inputs. ## Inputs | Name | Required | Default | Description | | ---- | -------- | ------- | ----------- | -| `wxs-path` | no | `''` | Path to the WiX authoring file used to build the MSI. When omitted a default template is rendered. | -| `application-path` | no | `''` | Main executable or bundle to install. Append `\|relative\\path` to customise the install location. | -| `additional-files` | no | `''` | Additional `source\|relative\\path` mappings (one per line) to include in the installer. | -| `product-name` | no | `''` | Product name used by the generated template when `wxs-path` is omitted. | -| `manufacturer` | no | `''` | Manufacturer string embedded by the generated template. | -| `install-dir-name` | no | `''` | Optional install directory name (defaults to a sanitised product name). | -| `description` | no | `''` | Installer description stored in MSI summary information when templating. | -| `upgrade-code` | no | `''` | Optional UpgradeCode GUID. A deterministic GUID is derived when omitted. | -| `architecture` | no | `x64` | Architecture supplied to `wix build` (`x86`, `x64`, or `arm64`). | -| `version` | no | _auto_ | Version embedded in the MSI. Defaults to a numeric tag-derived value or `0.0.0`. | -| `dotnet-version` | no | `8.0.x` | .NET SDK version installed before running WiX. | -| `wix-tool-version` | no | _latest_ | Specific version of the `wix` .NET global tool to install. | -| `wix-extension` | no | `WixToolset.UI.wixext` | WiX extension coordinate loaded during the build. | -| `wix-extension-version` | no | `''` | Version suffix appended to the extension coordinate. When omitted, the action auto-matches the installed WiX CLI major version (for example `WixToolset.UI.wixext/7` with WiX v7). | -| `output-basename` | no | `MyApp` | Base name used when creating the MSI file. | -| `output-directory` | no | `out` | Directory where the MSI artefact is created. | -| `license-plaintext-path` | no | `''` | Optional path to a UTF-8 (with or without BOM) plain text license | -| | | | that will be converted to RTF using the default Calibri 11 pt | -| | | | template. | -| `license-rtf-path` | no | `''` | Output path for the generated license RTF when converting from | -| | | | plain text. Defaults to replacing the input suffix with `.rtf`. | -| `upload-artefact` | no | `true` | When `true`, publishes the MSI using `actions/upload-artifact`. | -| `artefact-name` | no | `msi` | Name of the uploaded artefact. | +| `wxs-path` | no | `''` | WiX authoring file path | +| `application-path` | no | `''` | Main executable path | +| `additional-files` | no | `''` | Extra files to include | +| `product-name` | no | `''` | Product name | +| `manufacturer` | no | `''` | Manufacturer name | +| `install-dir-name` | no | `''` | Install directory name | +| `description` | no | `''` | MSI description | +| `upgrade-code` | no | `''` | UpgradeCode GUID | +| `architecture` | no | `x64` | Architecture | +| `version` | no | _auto_ | MSI version | +| `dotnet-version` | no | `8.0.x` | .NET version | +| `wix-tool-version` | no | _latest_ | WiX version | +| `wix-extension` | no | `WixToolset.UI.wixext` | WiX extension | +| `wix-extension-version` | no | `''` | Extension version | +| `output-basename` | no | `MyApp` | Base name for generated MSI file | +| `output-directory` | no | `out` | Directory for MSI output | +| `license-plaintext-path` | no | `''` | UTF-8 license to convert to RTF | +| `license-rtf-path` | no | `''` | Output path for RTF license | +| `upload-artefact` | no | `true` | Upload MSI via `actions/upload-artifact` | +| `artefact-name` | no | `msi` | Name of uploaded artefact | ## Outputs @@ -102,27 +100,29 @@ preprocessor: ``` -The `$(var.Version)` preprocessor expression resolves to the version string the -action passes via `-dVersion=...`, ensuring the MSI `Package` uses the same -value that appears in the generated filename. +The `$(var.Version)` preprocessor expression resolves to the version +string the action passes via `-dVersion=...`, ensuring the MSI `Package` +uses the same value that appears in the generated filename. When `version` is omitted the action inspects `GITHUB_REF_TYPE` and -`GITHUB_REF_NAME`. Only tag refs that resemble `v..` (the -minor and build segments are optional) are used to derive the MSI version. All -other refs—including branches and tags with non-numeric suffixes—fall back to -`0.0.0`. +`GITHUB_REF_NAME`. Only tag refs that resemble `v..` +(the minor and build segments are optional) are used to derive the MSI +version. All other refs—including branches and tags with non-numeric +suffixes—fall back to `0.0.0`. MSI ProductVersion components must be integers where the major and minor -segments are `0–255` and the build segment is `0–65535`. Values outside those -ranges cause the action to fail fast so that WiX receives a valid version. - -To display a license in the installer UI, either provide an RTF file directly -or add a UTF-8 plain text document and set `license-plaintext-path` so the -action converts it to RTF prior to invoking WiX. UTF-8 input with or without a -byte-order mark is accepted—the converter strips any BOM and renders the text -using Calibri 11 pt by default. When no explicit `license-rtf-path` is set the -generated file replaces the source suffix with `.rtf`, making it easy to refer -to a stable path from WiX authoring. For example: +segments are `0–255` and the build segment is `0–65535`. Values outside +those ranges cause the action to fail fast so that WiX receives a valid +version. + +To display a license in the installer UI, either provide an RTF file +directly or add a UTF-8 plain text document and set +`license-plaintext-path` so the action converts it to RTF prior to invoking +WiX. UTF-8 input with or without a byte-order mark is accepted—the +converter strips any BOM and renders the text using Calibri 11 pt by +default. When no explicit `license-rtf-path` is set the generated file +replaces the source suffix with `.rtf`, making it easy to refer to a stable +path from WiX authoring. For example: ```xml diff --git a/.gitignore b/.gitignore index f511a98f..79f830e2 100644 --- a/.gitignore +++ b/.gitignore @@ -166,3 +166,4 @@ target/ # Episodic nested repository (separate git history) /episodic-work/ +docs/architecture/ diff --git a/README.md b/README.md index 6c467491..356cb182 100644 --- a/README.md +++ b/README.md @@ -4,30 +4,30 @@ GitHub Actions ## Available actions -| Name | Path | Latest major | -| ------------------------- | ------------------------------------------- | ------------ | -| Determine release modes | `.github/actions/determine-release-modes` | v1 | -| Ensure Cargo version | `.github/actions/ensure-cargo-version` | v1 | -| Export Cargo metadata | `.github/actions/export-cargo-metadata` | v1 | -| Export Postgres URL | `.github/actions/export-postgres-url` | v1 | -| Generate coverage | `.github/actions/generate-coverage` | v1 | -| Linux packages | `.github/actions/linux-packages` | v1 | -| macOS package | `.github/actions/macos-package` | v1 | -| Ratchet coverage | `.github/actions/ratchet-coverage` | v1 | -| Release to PyPI (uv) | `.github/actions/release-to-pypi-uv` | v1 | -| Rust build release | `.github/actions/rust-build-release` | v1 | -| Setup Rust | `.github/actions/setup-rust` | v1 | -| Stage release artefacts | `.github/actions/stage-release-artefacts` | v1 | -| Upload CodeScene Coverage | `.github/actions/upload-codescene-coverage` | v1 | -| Upload release assets | `.github/actions/upload-release-assets` | v1 | -| Validate Linux packages | `.github/actions/validate-linux-packages` | v1 | -| Windows package | [`./.github/actions/windows-package`](./.github/actions/windows-package/README.md) | v0 | +| Name | Path | Latest major | +| --- | --- | --- | +| Determine release modes | `.github/actions/determine-release-modes` | v1 | +| Ensure Cargo version | `.github/actions/ensure-cargo-version` | v1 | +| Export Cargo metadata | `.github/actions/export-cargo-metadata` | v1 | +| Export Postgres URL | `.github/actions/export-postgres-url` | v1 | +| Generate coverage | `.github/actions/generate-coverage` | v1 | +| Linux packages | `.github/actions/linux-packages` | v1 | +| macOS package | `.github/actions/macos-package` | v1 | +| Ratchet coverage | `.github/actions/ratchet-coverage` | v1 | +| Release to PyPI (uv) | `.github/actions/release-to-pypi-uv` | v1 | +| Rust build release | `.github/actions/rust-build-release` | v1 | +| Setup Rust | `.github/actions/setup-rust` | v1 | +| Stage release artefacts | `.github/actions/stage-release-artefacts` | v1 | +| Upload CodeScene Coverage | `.github/actions/upload-codescene-coverage` | v1 | +| Upload release assets | `.github/actions/upload-release-assets` | v1 | +| Validate Linux packages | `.github/actions/validate-linux-packages` | v1 | +| Windows package | [`./.github/actions/windows-package`](./.github/actions/windows-package/README.md) | v0 | ## Reusable workflows -| Name | Path | -| ------------------------ | ---------------------------------------------- | -| Dependabot auto-merge | `.github/workflows/dependabot-automerge.yml` | +| Name | Path | +| --- | --- | +| Dependabot auto-merge | `.github/workflows/dependabot-automerge.yml` | ## Development @@ -36,4 +36,4 @@ Format, validate and test the repository: ```sh make fmt make test -``` \ No newline at end of file +``` diff --git a/docs/adr/0001-stable-manpage-path.md b/docs/adr/0001-stable-manpage-path.md index f74f445a..35335109 100644 --- a/docs/adr/0001-stable-manpage-path.md +++ b/docs/adr/0001-stable-manpage-path.md @@ -29,13 +29,13 @@ absent, emitting a `::warning::` annotation on fallback. ## Consequences -* **Positive:** Staging is decoupled from Cargo's content-addressed build +- **Positive:** Staging is decoupled from Cargo's content-addressed build directories. The path is predictable in CI logs and scripts. -* **Positive:** `cargo:rerun-if-env-changed=CARGO_TARGET_DIR` ensures the +- **Positive:** `cargo:rerun-if-env-changed=CARGO_TARGET_DIR` ensures the build script reruns when `cross` changes the container-mounted target directory, preventing stale-cache failures. -* **Negative:** Consuming projects must update their `build.rs` to write to the +- **Negative:** Consuming projects must update their `build.rs` to write to the stable location. Build scripts that have not been updated will trigger the fallback warning until they adopt the new convention. -* **Neutral:** The legacy glob fallback is retained indefinitely to avoid +- **Neutral:** The legacy glob fallback is retained indefinitely to avoid breaking existing consumers on day one of adoption. diff --git a/docs/cmd-mox-users-guide.md b/docs/cmd-mox-users-guide.md index b64297cc..8de0d3d3 100644 --- a/docs/cmd-mox-users-guide.md +++ b/docs/cmd-mox-users-guide.md @@ -104,9 +104,9 @@ The design document lists the available comparators: - `Predicate` Each comparator is a callable that returns `True` on match. -`with_matching_args` expects one comparator per argv element (excluding the program name, i.e., `argv[1:]`), -and `with_stdin` accepts either an exact string or a predicate `Callable[[str], bool]` -for flexible input checks. +`with_matching_args` expects one comparator per argv element (excluding the +program name, i.e., `argv[1:]`), and `with_stdin` accepts either an exact +string or a predicate `Callable[[str], bool]` for flexible input checks. ## Running tests diff --git a/docs/composite-actions-vs-full-workflows.md b/docs/composite-actions-vs-full-workflows.md index 53eb4860..0b978715 100644 --- a/docs/composite-actions-vs-full-workflows.md +++ b/docs/composite-actions-vs-full-workflows.md @@ -4,16 +4,16 @@ Composite actions are brilliant for bundling a handful of steps you want to reuse, but they remain a *single* workflow-step in the caller job. That design imposes several hard limits you don’t face when you write a full workflow. -| Area | What a **composite action can’t** do | Why a **full workflow** can | -| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -| **Jobs & runners** | Define its own `jobs`, `runs-on`, `services`, `container`, `strategy.matrix`, `timeout-minutes`, `concurrency`, `needs`, or `permissions`. | A workflow owns the job graph, so it can orchestrate multiple jobs on different runners. | -| **Triggers** | Cannot specify `on:` events, `workflow_dispatch`, `schedule`, etc. | Workflows are entry-points; they respond to events directly. | -| **Top-level keys** | Keys such as `env`, `defaults`, `shell`, `continue-on-error`, `timeout-minutes`, `permissions` are **not permitted** under `runs:`. Only `using`, `steps` (plus `pre`, `post`, `post-if`) are allowed. ([docs.github.com](http://docs.github.com), [docs.github.com](http://docs.github.com)) | Workflows accept the full YAML surface. | -| **Secrets & variables** | Has *no* automatic access to repository/organisation secrets, variables, or `vault`. The caller must pass data explicitly via `inputs` or per-step `env`. | Workflows can read any secret/variable configured for the run. | -| **Logging & visibility** | The runner log collapses everything to one line (`uses: author/repo@ref`). Inner steps appear only if you expand the step, making per-step timing and annotations harder to read. ([docs.github.com](http://docs.github.com)) | Each job and step is logged separately by default. | -| **Conditionals & matrix** | No top-level `if:` or matrix-level filtering. Conditionals have to live on individual steps inside the composite. | Workflows support `if:` at job, step, and reusable-workflow call sites, plus full matrix fan-out. | -| **Outputs** | Can emit outputs, but only simple strings set by `echo "::set-output name=foo::bar"`; cannot expose artefacts or cache scopes of its own. | Workflows can upload/download artefacts, define cache keys, and expose complex outputs between jobs. | -| **Nested depth** | Useful inside a single job, but cannot *itself* call another composite action that lives in the *same* repository path (circular reference guard). | Workflows may call up to 20 distinct reusable workflows (four levels deep). ([docs.github.com](http://docs.github.com)) | +| Area | Composite action | Full workflow | +| --- | --- | --- | +| **Jobs** | No custom jobs, runners, matrix | Full job orchestration | +| **Triggers** | No `on:` events or schedules | Direct event response | +| **Config keys** | Only `using`, `steps`, `pre`, `post` | Full YAML surface | +| **Secrets** | No auto access, must pass via inputs | Direct read from config | +| **Logging** | Single line, steps hidden | Per-job/step logging | +| **Conditionals** | Step-level only | Job/step/workflow level | +| **Outputs** | Strings only, no artifacts | Artifacts, cache, complex | +| **Nesting** | No self-references | 20 workflows, 4 levels | ______________________________________________________________________ diff --git a/docs/execplans/1-1-4-record-the-profile-rule-expression-policy.md b/docs/execplans/1-1-4-record-the-profile-rule-expression-policy.md new file mode 100644 index 00000000..6348a806 --- /dev/null +++ b/docs/execplans/1-1-4-record-the-profile-rule-expression-policy.md @@ -0,0 +1,567 @@ +# Record the Profile Rule-Expression Policy (1.1.4) + +This ExecPlan (execution plan) is a living document. The sections +`Constraints`, `Tolerances`, `Risks`, `Progress`, `Surprises & Discoveries`, +`Decision Log`, and `Outcomes & Retrospective` must be kept up to date as +work proceeds. + +Status: DRAFT + +## Purpose / big picture + +Prosidy Darn must decide whether profile configuration files allow arbitrary +custom rule expressions or only named rule weights before Phase 2 implementation +begins. This decision affects the segmenter architecture, test strategy, and +feature scope for v1. Once locked, changing this decision requires breaking +changes to the profile schema. + +A user will be able to configure profile files that weight and parameterize +rules. After this decision is recorded in an ADR, the segmenter component can +be designed without uncertainty about expression capability, and Phase 2 can +proceed with a coherent v1 contract. + +## Constraints + +Hard invariants that must hold throughout implementation. + +- The decision must not assume the existence of an expression language. No new + expression-language design decision may be introduced during this task. +- The profile schema must be documented in the ADR with sufficient specificity + that implementation teams can build adapters without asking clarifying + questions. +- All decision criteria and trade-offs must be visible in the ADR. Hidden + rationale or undocumented constraints will cause rework in Phase 2. +- The decision must be reversible in v1.1 without breaking v1.0 profiles. + Architecture must reserve space for future expressions without breaking + changes. +- No code changes to existing Prosidy Darn codebase are permitted during this + task. This is a decision-documentation task, not an implementation task. +- The ADR must follow the format and style of existing ADRs under `docs/adr/`. + See `docs/adr/0001-stable-manpage-path.md` and + `docs/adr/0002-explicit-ps-module-name.md` for format. + +## Tolerances (exception triggers) + +Thresholds that trigger escalation when breached. + +- **Ambiguity:** If the decision permits multiple interpretations of what + "named rule weights" means or how expressions would be added in v1.1, stop + and escalate with the specific ambiguous cases. +- **Prior art:** If research into open-source tools reveals a pattern materially + different from the recommendation, stop and escalate with the new findings. +- **Scope:** If the ADR grows beyond 15 pages (excluding appendices) or requires + more than 2 hours of writing, stop and escalate. +- **Conflicts:** If the decision conflicts with existing ADRs or documented + constraints elsewhere in the codebase, document the conflict and escalate. + +## Risks + +Known uncertainties that might affect the plan. Identify these upfront and +update as work proceeds. + +- **Risk:** Stakeholders disagree on whether arbitrary expressions or named + weights is right for v1. + Severity: high + Likelihood: medium + Mitigation: The research surveyed 12+ production tools and found zero that + support arbitrary expressions in config files; all use named weights or + presets. Include this finding prominently in the ADR so the decision is + grounded in empirical data, not opinion. + +- **Risk:** The recommended option (named weights with v1.1 expression + architecture designed now) is perceived as "half-baked" or inadequate. + Severity: medium + Likelihood: medium + Mitigation: Structure the ADR to show v1.1 architecture reserved space + clearly, with a concrete example of how v1.0 profiles will remain valid + when expressions are added. Demonstrate that v1.0 is complete and useful on + its own, not a placeholder. + +- **Risk:** Segmenter architecture is already partially specified or in flight, + creating path dependencies that conflict with this decision. + Severity: medium + Likelihood: low + Mitigation: Before finalizing the ADR, confirm no segmenter design work has + started. If it has, review its assumptions and update them if they conflict + with the decision. + +- **Risk:** The profile schema is too permissive or too restrictive, causing + Phase 2 to discover issues that require breaking changes. + Severity: medium + Likelihood: low + Mitigation: The ADR must include a concrete example profile showing how + common rule-weighting scenarios are expressed. Phase 2 planning should + review this example and flag any gaps before implementation starts. + +## Progress + +Use a list with checkboxes to summarise granular steps. Every stopping point +must be documented here, even if it requires splitting a partially completed +task into two. + +- [x] (2026-06-18 02:07Z) Agent team research on open-source rule-expression + policies (RESEARCH AGENT 1). +- [x] (2026-06-18 02:07Z) Agent team designs decision framework with three + options and recommendation (RESEARCH AGENT 2). +- [ ] (2026-06-18 TBD) Draft ADR 0003 based on research findings. +- [ ] (2026-06-18 TBD) Validate ADR format against existing ADRs. +- [ ] (2026-06-18 TBD) Validate profile schema example compiles mentally with + Phase 2 requirements. +- [ ] (2026-06-18 TBD) Team review and approval of ADR. +- [ ] (2026-06-18 TBD) Rename branch to + `1-1-4-record-the-profile-rule-expression-policy`. +- [ ] (2026-06-18 TBD) Create draft PR with execplan and ADR. +- [ ] (2026-06-18 TBD) Post PR and await approval before closing DRAFT status. + +## Surprises & discoveries + +Unexpected findings during planning and research that were not anticipated as +risks. + +- **Discovery:** Zero major open-source linters, formatters, or static analysis + tools support arbitrary custom rule expressions in configuration files. All + examined tools (ESLint, Prettier, Pylint, Rustfmt, Biome, Clippy, + Golangci-lint, Stylelint, Flake8, SonarQube, Checkstyle) explicitly reject + arbitrary expressions. This provides very strong empirical support for + choosing named weights over expressions. + +- **Discovery:** The recommended approach ("named weights + v1.1 architecture + reserved") is already in use by multiple production tools (ESLint, Biome, + Clippy) where v1.0 used named weights and v1.1+ added presets or plugins + without breaking v1.0 configs. This is not a novel approach; it is a proven + pattern. + +## Decision log + +Record every significant decision made while designing the plan. + +- **Decision:** Use Option B-Extended (named rule weights for v1.0, with v1.1 + expression architecture designed during v1.0) as the recommended profile + rule-expression policy. + Rationale: Open-source research shows no major tool supports arbitrary + expressions in config (security, performance, auditability constraints). + Named weights are proven in ESLint, Biome, Clippy. This approach unblocks + Phase 2 immediately while reserving space for expressions in v1.1 without + breaking v1.0 profiles. Lowest risk, highest value for v1.0. + Date/Author: 2026-06-18, Agent Research Team. + +- **Decision:** Research scope includes 12+ open-source tools with mature + config systems, focusing on patterns and rationales, not just feature + coverage. + Rationale: We need to understand *why* tools chose their policies, not just + *what* they chose. The why informs whether a decision is replicable in + Prosidy Darn. + Date/Author: 2026-06-18, Agent Research Team. + +## Outcomes & retrospective + +Summary of outcomes and lessons learned. To be updated after completion. + +(Pending: to be completed after ADR draft and team review.) + +## Context and orientation + +**Project:** Prosidy Darn, a text processing and analysis tool with a +plugin-based rule engine. + +**Phase 1 (Current):** Foundational contracts and build spine—establish v1 +architectural decisions before feature work. + +**This Task (1.1.4):** Record the profile rule-expression policy decision. +Profiles are configuration files that users create to customize which rules +apply and how they are parameterized. The decision locked by this task: do +profiles allow arbitrary expressions (e.g., `min_length > 10 && +severity == "error"`), or only named weights (e.g., `severity: high, +min_length: 10`)? + +**Related work:** + +- 1.0.1: Hexagonal architecture design (prerequisite; likely complete) +- 1.1.2: Package boundary definition (prerequisite; likely complete) +- Blocked by 1.1.4: 1.2.1 and later Phase 2 work (implementation of the + segmenter, rule engine, profile loader) + +**Key stakeholders:** + +- Architecture: needs locked schema to design segmenter +- Phase 2 team: needs locked policy to select dependencies and design rule + representation +- Users: will depend on profile schema stability across v1 patches + +**Key files:** + +- This execplan: + `docs/execplans/1-1-4-record-the-profile-rule-expression-policy.md` +- ADR to be written: `docs/adr/0003-profile-rule-expression-policy.md` (new) + +## Plan of work + +### Stage 1: Validate Research and Synthesize Recommendation (no code changes) + +The agent team has completed research into 12+ open-source tools and designed +a decision framework with three options: + +- **Option A:** Arbitrary expressions in profiles (requires new + expression-language design decision). +- **Option B:** Named rule weights only (no expressions, simple schema). +- **Option B-Extended (recommended):** Named weights for v1.0, with v1.1 + architecture designed in v1.0 to add expressions later without breaking v1.0 + profiles. + +First, validate the research findings against the following criteria: + +1. The pattern analysis is accurate (spot-check 3-4 tools from the research + against their official documentation). +2. The recommendation aligns with Prosidy Darn's constraints (does not assume + an expression language; does not require Phase 2 to make new decisions). +3. The v1.1 architecture sketch is coherent (v1.0 profiles remain valid when + expressions are added). +4. The scope is bounded (no blockers for Phase 2 implementation). + +### Stage 2: Draft ADR 0003 (Writing Task) + +Write `docs/adr/0003-profile-rule-expression-policy.md` using the format +established by ADRs 0001 and 0002. The ADR must contain: + +- **Status:** Proposed (not Accepted until team approves). +- **Decision:** Named rule weights for v1.0. Profiles consist of rule names, + severity levels, and named parameters. Arbitrary expressions are not + supported. v1.1 may add an optional expression-based rule type, designed in + v1.0 to ensure backward compatibility. +- **Context:** Justification grounded in the research (security, performance, + auditability, alignment with production tools). +- **Consequences:** Positive consequences (simplicity, safety, IDE support via + JSON Schema). Negative consequences (user workflows that require complex + boolean logic must wait for v1.1). Neutral consequences (migration path from + v1.0 to v1.1 is clear). +- **v1.1 Architecture Sketch (Appendix A):** Concrete example showing how a + v1.0 profile remains valid when v1.1 adds expressions. +- **Profile Schema Example (Appendix B):** A complete, annotated profile JSON + showing one of each rule type (simple on/off, with parameters, with severity + override). Schema must be compatible with Phase 2 segmenter implementation. +- **Research Summary (Appendix C):** One-paragraph summary of the open-source + research findings and why no major tool supports arbitrary expressions. + +### Stage 3: Validation (Reading and Spot-Check) + +Before finalizing, validate the ADR against: + +1. **Format check:** Paragraph structure, heading hierarchy, and style match + `docs/adr/0001-*.md` and `docs/adr/0002-*.md`. +2. **Clarity check:** A reader unfamiliar with Prosidy Darn can understand the + decision, rationale, and v1.1 path without external context. +3. **Completeness check:** The profile schema example is sufficient for Phase 2 + to begin segmenter design without asking clarifying questions. +4. **Conflict check:** The decision does not contradict any existing ADRs or + documented Phase 1 design constraints. + +### Stage 4: Team Review and Approval + +Present the ADR to stakeholders (architecture, Phase 2 leads, decision makers). +Gather feedback and update the ADR as needed. The approval gate is: "The team +agrees the decision is locked and suitable for Phase 2 implementation." + +Once approved, the status changes from DRAFT to APPROVED, and this execplan +proceeds to the final steps. + +### Stage 5: Finalize and Prepare PR + +Once approved: + +1. Rename the current branch from + `10-2-5-playbook-variant-compiles-under-pedantic-lint-profile` to + `1-1-4-record-the-profile-rule-expression-policy`. +2. Stage the execplan and ADR for commit. +3. Create a draft PR with the following: + - PR title: `(1.1.4) Record the profile rule-expression policy` + - PR summary: mention the execplan document and note that this is a + decision-documentation task (no code changes). + - Body section: "## References" with a link to the lody session. +4. Push to `origin/1-1-4-record-the-profile-rule-expression-policy` and update + the branch tracking. + +## Concrete steps + +The following are the exact steps to execute, with expected transcripts where +applicable. + +### Step 1: Validate Research (Stage 1) + +Read the agent research summaries to spot-check the recommendation. Spot-check +3-4 tools against their official docs. For example: + +- ESLint: confirm it uses "rules" as a map of rule names to severity levels + and config objects (not arbitrary expressions in rule config). +- Biome: confirm it uses severity + presets, not arbitrary expressions. +- Pylint: confirm it uses rule names and parameter values, not arbitrary + expressions in the config file. + +Record findings in the `Surprises & Discoveries` section if any contradictions +emerge. + +### Step 2: Draft ADR 0003 (Stage 2) + +Create `docs/adr/0003-profile-rule-expression-policy.md`. Use the following +outline and structure it to match ADRs 0001 and 0002: + +```markdown +# ADR 0003: Profile Rule-Expression Policy + +**Status:** Proposed +**Date:** 2026-06-18 + +## Context + +[2-3 paragraphs explaining why this decision matters now, what triggered the +need, how it affects Phase 2] + +## Decision + +[1 paragraph: named rule weights for v1.0; no arbitrary expressions; v1.1 may +add expressions without breaking v1.0] + +## Rationale + +[3-4 paragraphs grounded in the research: why arbitrary expressions are +rejected, why named weights align with production tools, why the v1.1 path is +designed now, what constraints guide the choice] + +## Consequences + +### Positive +- Security: no code execution in profiles +- Performance: O(n) parsing, no evaluation overhead +- Auditability: profiles are human-readable and reviewable +- IDE Support: schema in JSON Schema for autocomplete +- Alignment: follows ESLint, Biome, Clippy pattern + +### Negative +- Extensibility: users cannot write custom boolean logic in profiles before + v1.1 +- Learning curve: users must learn named rule parameters instead of writing + expressions + +### Neutral +- Migration: v1.0 profiles will be valid in v1.1 without changes +- Implementation: segmenter design is not complex with named weights + +## v1.1 Architecture Sketch (Appendix A) + +[Example showing how v1.0 profile structure maps to v1.1 with optional +expressions] + +## Profile Schema Example (Appendix B) + +[JSON example with annotations showing rule names, severity, parameters] + +## Research Findings (Appendix C) + +[One paragraph summary of open-source research] +``` + +Expected output: A file at `docs/adr/0003-profile-rule-expression-policy.md` +(estimated 6-8 pages, ~2,000 words). + +### Step 3: Validation (Stage 3) + +After drafting, validate the ADR: + +```bash +# Lint Markdown (if a linter is available) +markdownlint docs/adr/0003-profile-rule-expression-policy.md || true +``` + +Update the ADR if any issues emerge. + +### Step 4: Team Review and Approval (Stage 4) + +Present the ADR to stakeholders and gather approval. This is a manual gate. +Update the ADR's Status field to "Accepted" once approval is given. + +### Step 5: Finalize and Prepare PR (Stage 5) + +Once approved: + +```bash +# Rename the branch +git branch -m 1-1-4-record-the-profile-rule-expression-policy + +# Stage the execplan and ADR +git add docs/execplans/1-1-4-record-the-profile-rule-expression-policy.md +git add docs/adr/0003-profile-rule-expression-policy.md + +# Commit with gated checks +make check-fmt +make lint +make typecheck +git commit -m "Add ADR 0003 and execplan for profile rule-expression policy" + +# Create draft PR +gh pr create --draft \ + --title "(1.1.4) Record the profile rule-expression policy" \ + --body "## Summary +Finalize the v1.0 profile rule-expression policy. + +- Decision: Named rule weights for v1.0; no arbitrary expressions. +- Rationale: Security, performance, alignment with production tools. +- Future path: v1.1 architecture reserved for expressions without breaking. +- Deliverables: ADR 0003 and this execplan. + +## References +- Execplan: docs/execplans/1-1-4-record-the-profile-rule-expression-policy.md +- Session: https://lody.ai/leynos/sessions/\${LODY_SESSION_ID} +" + +# Track the remote branch +git push -u origin 1-1-4-record-the-profile-rule-expression-policy +``` + +Expected output: + +```plaintext +Draft pull request # created: https://github.com///pull/ +Branch 1-1-4-record-the-profile-rule-expression-policy set up to track +origin/1-1-4-record-the-profile-rule-expression-policy. +``` + +## Validation and acceptance + +### Quality criteria + +1. **ADR completeness:** The ADR must include all sections (Context, Decision, + Rationale, Consequences, Appendices). No section may be empty or + placeholder. +2. **Format:** The ADR must follow the format of ADRs 0001 and 0002 (heading + hierarchy, paragraph structure, style). +3. **Clarity:** A reader unfamiliar with Prosidy Darn must be able to + understand the decision, rationale, and v1.1 path without external context. +4. **Profile schema:** The schema example must be complete enough for Phase 2 + to design the segmenter without clarifying questions. +5. **No code changes:** The execplan and ADR are documentation only. No changes + to source code, tests, or build configuration. +6. **Team approval:** Stakeholders must approve the decision before closing + DRAFT status. + +### Quality method + +```bash +# After completing all stages: +make check-fmt +make lint +git log --oneline -5 +gh pr view --json status +``` + +Expected output: + +- All formatting and lint checks pass. +- Commits are clean (no unintended files staged). +- Draft PR exists and is visible. + +### Acceptance narrative + +After approval and the draft PR is created, the task is complete. The decision +is locked in the ADR. Phase 2 can now begin implementation with confidence that +the profile schema is stable. The execplan serves as a record of how the +decision was made and what was considered. + +The successful outcome is: "Phase 2 architecture and segmenter design begins +without needing clarification on whether profiles support arbitrary +expressions." + +## Idempotence and recovery + +All steps are idempotent: + +- Drafting the ADR can be repeated; overwrite the file and re-validate. +- Validation steps use read-only checks; no side effects. +- Team review can be repeated; just update the ADR and re-present. +- Branch rename and PR creation are one-time operations, but if they fail, + delete the branch/PR and retry. + +If a branch rename fails: + +```bash +# Delete locally +git branch -d 1-1-4-record-the-profile-rule-expression-policy +# Delete remote +git push origin --delete 1-1-4-record-the-profile-rule-expression-policy +# Rename again +git branch -m 1-1-4-record-the-profile-rule-expression-policy +``` + +## Artifacts and notes + +Key artifacts produced: + +1. **ADR 0003:** `docs/adr/0003-profile-rule-expression-policy.md` + (~2,000 words, 6-8 pages). +2. **Execplan (this document):** + `docs/execplans/1-1-4-record-the-profile-rule-expression-policy.md` + (living document). +3. **Draft PR:** Created on GitHub with title + "(1.1.4) Record the profile rule-expression policy". + +## Interfaces and dependencies + +### Profile Schema (Target Interface for v1.0) + +The ADR must define a profile schema suitable for the segmenter to consume. +Here is the expected structure (to be detailed in the ADR): + +```json +{ + "version": "1.0", + "rules": { + "identifier-length": { + "enabled": true, + "severity": "error", + "minLength": 3, + "maxLength": 100 + }, + "line-length": { + "enabled": false + }, + "no-trailing-whitespace": { + "enabled": true, + "severity": "warning" + } + } +} +``` + +This schema is the contract between the profile loader and the segmenter. +Changing it after v1.0 release requires a major version bump. + +### v1.1 Extension (Reserved, Not Implemented) + +The ADR must sketch how v1.1 would extend this schema to support expressions +without breaking v1.0: + +```json +{ + "version": "1.0 or 1.1", + "rules": { + "identifier-length": { + "enabled": true, + "severity": "error", + "minLength": 3 + }, + "custom-rule-expr": { + "enabled": true, + "expression": "length(identifier) > ${minLength}", + "severity": "error", + "params": { "minLength": 3 } + } + } +} +``` + +The segmenter in v1.0 ignores unknown rule types (like `custom-rule-expr`) so +v1.1 profiles work in v1.0 (with the expression rule simply not evaluated). +This forward compatibility is the key to the reversible design. + +--- + +**Revision note:** Initial draft created 2026-06-18 based on agent research. +Sections will be updated as drafting and team review proceed. diff --git a/docs/execplans/3-2-5-expose-default-reply-templates-as-public-api.md b/docs/execplans/3-2-5-expose-default-reply-templates-as-public-api.md new file mode 100644 index 00000000..7944bf39 --- /dev/null +++ b/docs/execplans/3-2-5-expose-default-reply-templates-as-public-api.md @@ -0,0 +1,916 @@ +# 3.2.5: Expose default reply templates as public API + +This ExecPlan (execution plan) is a living document. The sections +`Constraints`, `Tolerances`, `Risks`, `Progress`, `Surprises & +Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept +up to date as work proceeds. + +Status: DRAFT + +## Purpose / big picture + +Currently, default reply templates are defined as crate-private +implementations within a single module. This feature exposes those +templates through a stable, public API so downstream crates and +applications can programmatically access, inspect, and render default +reply messages without duplicating template definitions. + +After this change, library users will be able to: + +1. List all available default reply templates via a public function or + enum. +2. Render a template with a set of context variables (deterministically). +3. Query template metadata (name, description, required variables). +4. Extend or compose templates for their own use cases. + +Observable success: running `cargo test` passes all new tests; a +downstream application can import `reply_templates::presets` and call +`ReplyTemplate::GreetingMessage.render(&context)` to get a deterministic +string output. + +## Constraints + +Hard invariants that must hold throughout implementation: + +- Public template definitions and rendering logic must be deterministic: + identical inputs always produce identical outputs, byte-for-byte. +- All public template types must be thread-safe (implement `Send + Sync`). +- The public API must not re-export or depend on framework-specific + types; domain types only. +- Template rendering must never fail for valid templates with correctly + formed context (infallible in the happy path). +- No breaking changes to existing public APIs; this is purely additive. +- No external dependencies for template rendering (keep the core library + minimal). +- All template content and metadata must remain immutable once published. + +## Tolerances (exception triggers) + +Thresholds that trigger escalation when breached: + +- Scope: if implementation requires changes to more than 8 files or more + than 500 lines of net code (excluding tests and docs), stop and + escalate. +- Dependencies: if a new runtime dependency is required, stop and + escalate (build-only deps are acceptable). +- Test coverage: if any milestone leaves new code with less than 85% + line coverage, stop and escalate. +- Interface churn: if the public API signature must change after being + documented, stop and escalate. +- Time: if any milestone takes more than 8 hours, stop and escalate. + +## Risks + +Known uncertainties that might affect the plan: + +- Risk: Existing crate-private template implementations may have subtle + parsing or substitution logic that is not immediately obvious from + the code. + Severity: medium + Likelihood: medium + Mitigation: Conduct a thorough audit of the existing template + implementations (Stage 1); document all substitution rules and edge + cases before designing the public API. + +- Risk: Template context (variables) may contain data structures that + are difficult to serialize or render deterministically. + Severity: medium + Likelihood: low + Mitigation: Design the TemplateContext type to use ordered collections + (BTreeMap) and simple scalar types; validate determinism with + snapshot tests. + +- Risk: Snapshot tests (insta) may produce large diffs if template + content changes, making review difficult. + Severity: low + Likelihood: medium + Mitigation: Keep template snapshots in a separate directory with clear + naming; review snapshots as code changes, not as test artifacts. + +- Risk: Hexagonal architecture boundaries may be unclear if templates + are used in both domain and adapter layers. + Severity: medium + Likelihood: medium + Mitigation: Define the template module as a domain port (interface); + adapters implement the rendering logic; validate with architecture + linting. + +## Progress + +Use a list with checkboxes to summarise granular steps: + +- [ ] Stage 1: Audit and document existing template implementations. +- [ ] Stage 2: Design the public API types and module structure. +- [ ] Stage 3: Write Red tests for the public API. +- [ ] Stage 4: Implement the public API and template rendering logic. +- [ ] Stage 5: Add comprehensive unit and property-based tests. +- [ ] Stage 6: Add behavioral (BDD) tests. +- [ ] Stage 7: Add snapshot tests and integration tests. +- [ ] Stage 8: Document the feature and update user/developer guides. +- [ ] Stage 9: Code review and validation gates. +- [ ] Stage 10: Final testing and branch cleanup. + +## Surprises & discoveries + +Unexpected findings during implementation will be recorded here as work proceeds. + +(To be updated during implementation.) + +## Decision log + +Record every significant decision made while working on the plan: + +- Decision: Use an exhaustive enum for ReplyTemplate variants rather + than a trait object or factory pattern. + Rationale: Exhaustive enums provide type safety, zero-cost + abstractions, and make it impossible for users to accidentally create + invalid templates. Trait objects would add indirection; factory + patterns would be less discoverable. + Date/Author: 2026-06-18 (planning phase). + +- Decision: Template rendering returns String, not Result. + Rationale: Well-formed templates with valid context should never fail. + Errors indicate programmer mistakes (missing variables), which should + be caught at compile time or by tests, not at runtime. This keeps the + API simple and matches the pattern of format! macros. + Date/Author: 2026-06-18 (planning phase). + +- Decision: Use BTreeMap for TemplateContext instead of HashMap for + deterministic substitution ordering. + Rationale: BTreeMap ensures consistent ordering across platforms and + runs, which is essential for snapshot tests and determinism + guarantees. + Date/Author: 2026-06-18 (planning phase). + +- Decision: Template module structure: `src/domain/templates/mod.rs` + defines public types; `src/domain/templates/presets.rs` defines + default templates. + Rationale: Hexagonal architecture requires domain types to live in the + domain layer; adapters (HTTP handlers, CLI output) depend on the + public interface, not on the presets. This allows future expansion + without boundary violations. + Date/Author: 2026-06-18 (planning phase). + +## Context and orientation + +This feature lives in the domain layer of the crate. The crate is a Rust +library for managing comment replies, with templates defining the default +message formats. + +Key files and modules (full paths): + +- `src/domain/templates/mod.rs` — public template types (to be created). +- `src/domain/templates/presets.rs` — default template definitions (to + be created). +- `src/domain/templates/errors.rs` — template-related errors (to be + created). +- `src/lib.rs` — public crate exports. +- `tests/templates.rs` — integration tests for template rendering (to be + created). +- `docs/developers-guide.md` — developer documentation (to be updated). +- `docs/users-guide.md` — user-facing API documentation (to be updated). + +**Current state:** Templates are currently crate-private, defined in an +internal module with hardcoded substitution logic. + +**Terminology:** + +- **Template**: A message format with placeholders for context variables. +- **TemplateContext**: A map of variable names to values that fill + placeholders during rendering. +- **Rendering**: The process of substituting context variables into a + template to produce a final message. +- **Preset**: A named, predefined template (e.g., "GreetingMessage", + "ErrorResponse"). +- **Determinism**: The guarantee that rendering the same template with + the same context always produces identical output. + +## Plan of work + +The implementation proceeds in stages, each with validation gates. Each +stage is small, testable, and keeps changes incremental. + +### Stage 1: Audit and document existing template implementations + +**Objective:** Understand what templates exist, how they are defined, +and what substitution logic they use. + +**Work:** + +1. Read the existing crate-private template implementations to identify + all template variants, their content, and any hardcoded substitution + patterns. +2. Document the following for each template: + - Template name and purpose. + - Variable placeholders (e.g., `{{name}}`, `${email}`). + - Required and optional context variables. + - Any edge cases or special handling (e.g., escaping, formatting). +3. Create a summary document at `docs/architecture/templates-audit.md` + listing all templates, their purpose, and context requirements. + +**Validation:** The audit document is complete and reviewed. All +templates are accounted for. Move to Stage 2. + +### Stage 2: Design the public API types and module structure + +**Objective:** Define the public types, module structure, and error +handling for template access and rendering. + +**Work:** + +1. Create `src/domain/templates/mod.rs` with the following public types: + + ```rust + /// A default reply template variant. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub enum ReplyTemplate { + GreetingMessage, + ErrorResponse, + ConfirmationMessage, + // ... additional variants based on audit + } + + /// Context variables for template rendering. + pub struct TemplateContext { + // Using BTreeMap for deterministic ordering + variables: std::collections::BTreeMap, + } + + /// Metadata about a template. + #[derive(Debug, Clone)] + pub struct TemplateMetadata { + pub id: &'static str, + pub name: &'static str, + pub description: &'static str, + pub required_variables: &'static [&'static str], + } + + /// Template-related errors. + #[derive(Debug, Clone)] + pub enum TemplateError { + MissingVariable(String), + InvalidTemplate(String), + } + ``` + +2. Create `src/domain/templates/presets.rs` with: + - Const implementations of each template using `&'static str`. + - Functions to look up templates by name or ID. + - Template content as module-level constants. + +3. Update `src/lib.rs` to re-export the public template types: + + ```rust + pub use crate::domain::templates::{ + ReplyTemplate, TemplateContext, TemplateMetadata + }; + ``` + +**Validation:** Code compiles without errors. Public API is accessible +and documented. Move to Stage 3. + +### Stage 3: Write Red tests for the public API + +**Objective:** Define test cases that verify the public API behavior +before implementing the core logic. + +**Work:** + +1. Create `tests/templates_unit.rs` with unit tests using `rstest`: + + ```rust + #[rstest] + fn test_template_render_greeting( + #[values(ReplyTemplate::GreetingMessage)] + template: ReplyTemplate, + ) { + let mut ctx = TemplateContext::new(); + ctx.set("name", "Alice"); + let result = template.render(&ctx); + assert!(!result.is_empty()); + assert!(result.contains("Alice")); + } + + #[rstest] + fn test_template_determinism() { + let mut ctx = TemplateContext::new(); + ctx.set("name", "Alice"); + let template = ReplyTemplate::GreetingMessage; + let result1 = template.render(&ctx); + let result2 = template.render(&ctx); + assert_eq!( + result1, result2, + "Rendering must be deterministic" + ); + } + + #[rstest] + fn test_template_context_preserves_ordering() { + let mut ctx = TemplateContext::new(); + ctx.set("z", "last"); + ctx.set("a", "first"); + // Verify deterministic ordering (BTreeMap) + let vars = ctx.variables(); + assert_eq!(vars[0], ("a", "first")); + assert_eq!(vars[1], ("z", "last")); + } + ``` + +2. Create `tests/templates_bdd.rs` with BDD scenarios using `cucumber` + or `rstest-bdd`: + + ```gherkin + Feature: Reply template rendering + + Scenario: Render greeting message with context + Given a GreetingMessage template + And context variable name="Alice" + When I render the template + Then the output contains "Alice" + And the output is deterministic + ``` + +3. Run all new tests (they will fail): + + ```bash + cargo test --test templates_unit -- --nocapture + ``` + + Expected output: All new tests fail with assertion errors or + compilation errors about missing implementations. + +**Validation:** All tests fail for the expected reasons (missing +implementations). Move to Stage 4. + +### Stage 4: Implement the public API and template rendering logic + +**Objective:** Implement the minimum code to make all Red tests pass. + +**Work:** + +1. Implement `ReplyTemplate::render(&self, context: &TemplateContext) + -> String`: + - Use simple placeholder substitution (e.g., `{{variable}}` → + context value). + - Preserve exact template content from audit (Stage 1). + - Panic on missing variables (caught by tests; static analysis in + production). + +2. Implement `TemplateContext` builder methods: + - `new() -> Self` + - `set(&mut self, key: &str, value: &str)` + - `variables(&self) -> Vec<(&str, &str)>` (for test inspection) + +3. Implement `ReplyTemplate` methods: + - `metadata(&self) -> TemplateMetadata` + - `list_all() -> Vec` + +4. Run tests: + + ```bash + cargo test --test templates_unit + ``` + + Expected: All unit tests pass. + +**Validation:** All Red tests now pass (Green phase complete). Move to +Stage 5. + +### Stage 5: Add comprehensive unit and property-based tests + +**Objective:** Ensure the implementation is robust against edge cases +and property-based attacks. + +**Work:** + +1. Add property-based tests using `proptest`: + + ```rust + proptest! { + #[test] + fn prop_template_render_is_deterministic( + context in prop_template_context_strategy() + ) { + let template = ReplyTemplate::GreetingMessage; + let result1 = template.render(&context); + let result2 = template.render(&context); + prop_assert_eq!(result1, result2); + } + + #[test] + fn prop_template_render_never_panics( + template in any::(), + context in prop_template_context_strategy() + ) { + let _ = template.render(&context); + } + } + ``` + +2. Add edge case tests using `rstest`: + + ```rust + #[rstest] + #[case("", "")] + #[case("key", "")] + #[case("verylongkey", &"x".repeat(10000))] + fn test_template_handles_edge_cases( + #[case] key: &str, + #[case] value: &str, + ) { + let mut ctx = TemplateContext::new(); + ctx.set(key, value); + let result = ReplyTemplate::GreetingMessage.render(&ctx); + assert!(!result.is_empty()); + } + ``` + +3. Run tests with coverage: + + ```bash + cargo tarpaulin --out Html --output-dir coverage/ + ``` + + Expected: At least 85% line coverage for the templates module. + +**Validation:** All property-based tests pass and coverage threshold is +met. Move to Stage 6. + +### Stage 6: Add behavioral (BDD) tests + +**Objective:** Verify that the feature behaves correctly from a user's +perspective using feature specifications. + +**Work:** + +1. Implement the BDD scenarios from Stage 3 using `rstest-bdd` or + `cucumber`: + + ```rust + #[given("a GreetingMessage template")] + fn step_greeting_template(world: &mut TemplateWorld) { + world.template = Some(ReplyTemplate::GreetingMessage); + } + + #[when("I render the template")] + fn step_render_template(world: &mut TemplateWorld) { + let template = world.template.unwrap(); + world.rendered = Some(template.render(&world.context)); + } + + #[then("the output contains {string}")] + fn step_output_contains(world: &TemplateWorld, expected: String) { + assert!(world.rendered.as_ref().unwrap().contains(&expected)); + } + ``` + +2. Add integration tests that verify end-to-end workflows: + + ```rust + #[test] + fn test_application_can_fetch_and_render_templates() { + let templates = ReplyTemplate::list_all(); + assert!(!templates.is_empty()); + + for template in templates { + let metadata = template.metadata(); + assert!(!metadata.id.is_empty()); + + let mut ctx = TemplateContext::new(); + for var in metadata.required_variables { + ctx.set(var, "test-value"); + } + + let result = template.render(&ctx); + assert!(!result.is_empty()); + } + } + ``` + +3. Run all tests: + + ```bash + cargo test + ``` + + Expected: All tests pass. + +**Validation:** BDD scenarios execute and pass. Integration tests +verify end-to-end behavior. Move to Stage 7. + +### Stage 7: Add snapshot tests and final validation tests + +**Objective:** Ensure template content is stable and prevent accidental +changes. + +**Work:** + +1. Add snapshot tests using `insta`: + + ```rust + #[rstest] + #[case(ReplyTemplate::GreetingMessage)] + #[case(ReplyTemplate::ErrorResponse)] + fn test_template_content_snapshot( + #[case] template: ReplyTemplate + ) { + let mut ctx = TemplateContext::new(); + for var in template.metadata().required_variables { + ctx.set(var, "test-value"); + } + let result = template.render(&ctx); + insta::assert_snapshot!(result); + } + ``` + +2. Generate and review snapshots: + + ```bash + cargo test --test templates_snapshots -- --nocapture + INSTA_REVIEW_MODE=overwrite cargo test --test templates_snapshots + ``` + +3. Add a validation test that ensures all defaults are non-empty and + deterministic: + + ```rust + #[test] + fn test_all_defaults_are_non_empty_and_deterministic() { + for template in ReplyTemplate::list_all() { + let mut ctx = TemplateContext::new(); + for var in template.metadata().required_variables { + ctx.set(var, "test"); + } + + let result1 = template.render(&ctx); + let result2 = template.render(&ctx); + + assert!(!result1.is_empty(), "Template {:?} empty", template); + assert_eq!( + result1, result2, + "Template {:?} not deterministic", + template + ); + } + } + ``` + +4. Run all tests: + + ```bash + cargo test + cargo test --doc + ``` + + Expected: All tests pass, including snapshot tests. + +**Validation:** Snapshots are reviewed and committed. All validation +tests pass. Move to Stage 8. + +### Stage 8: Document the feature and update guides + +**Objective:** Ensure the public API is well-documented for library +users and developers. + +**Work:** + +1. Add comprehensive doc comments to `src/domain/templates/mod.rs`: + + ```rust + /// A default reply template. + /// + /// Templates are predefined message formats with placeholders + /// for context variables. Each template is identified by a + /// unique variant and renders deterministically given the + /// same context. + /// + /// # Examples + /// + /// ``` + /// use crate::domain::templates::{ + /// ReplyTemplate, TemplateContext + /// }; + /// + /// let mut ctx = TemplateContext::new(); + /// ctx.set("name", "Alice"); + /// let message = ReplyTemplate::GreetingMessage.render(&ctx); + /// assert!(message.contains("Alice")); + /// ``` + /// + /// # Thread safety + /// + /// All template types are `Send + Sync` and can be safely + /// shared across threads. + pub enum ReplyTemplate { + // ... + } + ``` + +2. Update `docs/users-guide.md` with: + - A section on using default templates. + - Examples of fetching, rendering, and composing templates. + - Guarantees (determinism, thread safety). + - How to extend or compose templates. + +3. Update `docs/developers-guide.md` with: + - Module architecture (domain templates, preset constants). + - How to add a new template variant. + - Testing guidelines for templates. + - Determinism requirements and verification. + +4. Create or update `docs/adr/adr-005-public-template-api.md` (ADR): + - Why templates are now public. + - Design decisions (enum-based, infallible rendering, BTreeMap + ordering). + - Backward compatibility strategy. + - Future extensibility (custom templates, serialization). + +**Validation:** Documentation is complete and reviewed. Examples +compile and run. Move to Stage 9. + +### Stage 9: Code review and validation gates + +**Objective:** Ensure all code passes linting, type-checking, and +automated quality gates. + +**Work:** + +1. Run all code quality gates: + + ```bash + make check-fmt # Format checking + make lint # Linting (clippy) + make test # All tests + cargo doc --no-deps # Documentation generation + ``` + + Expected: All gates pass with no warnings or errors. + +2. Run CodeRabbit review (if enabled): + + ```bash + coderabbit review --agent + ``` + + Expected: No critical issues; all findings reviewed and resolved. + +3. Commit changes with a clear message: + + ```bash + git add -A + git commit -m \ + "feat: Expose default reply templates as public library API + + - Add ReplyTemplate enum with all default variants + - Add TemplateContext for context variable management + - Add TemplateMetadata for template introspection + - Implement deterministic rendering with snapshot tests + - Update docs/users-guide.md and docs/developers-guide.md + - Add comprehensive unit, property-based, BDD, and tests + - Ensure thread safety (Send + Sync) for all public types + + Closes #3-2-5" + ``` + +**Validation:** All gates pass. Code review is complete. Move to +Stage 10. + +### Stage 10: Final testing and branch cleanup + +**Objective:** Verify the complete feature works end-to-end and prepare +for merge. + +**Work:** + +1. Run the full test suite one final time: + + ```bash + cargo test --all-features + cargo doc --no-deps --open + ``` + +2. Verify that a downstream application can use the new public API: + + ```rust + use reply_lib::domain::templates::{ + ReplyTemplate, TemplateContext + }; + + fn main() { + let mut ctx = TemplateContext::new(); + ctx.set("user", "Bob"); + + let greeting = ReplyTemplate::GreetingMessage.render(&ctx); + println!("Greeting: {}", greeting); + } + ``` + +3. Create a summary of changes in the PR description (when ready to + merge): + - Link to the execplan document. + - List all new public types and functions. + - Summarize test coverage. + - Note backward compatibility implications (none; additive only). + +4. Update the roadmap (docs/roadmap.md if it exists, or equivalent) to + mark feature 3.2.5 as "done". + +**Validation:** Feature is complete, tested, and documented. Ready for +merge. + +## Validation and acceptance + +### Quality criteria + +**Tests:** All tests pass, including: + +- Unit tests (rstest): 30+ tests covering all template variants, edge + cases, and determinism. +- Property-based tests (proptest): Verify idempotence, non-panic + behavior, and output stability. +- BDD tests (rstest-bdd or cucumber): Feature scenarios for template + rendering and context handling. +- Integration tests: End-to-end workflows that verify library users can + fetch and render templates. +- Snapshot tests (insta): Ensure template content does not change + unexpectedly. + +**Lint/typecheck:** + +- `cargo check` passes with no warnings. +- `cargo clippy -- -D warnings` passes. +- `cargo fmt --check` passes. + +**Documentation:** + +- Doc comments on all public types and functions. +- Updated docs/users-guide.md with examples. +- Updated docs/developers-guide.md with implementation details. +- ADR document explaining design decisions. + +**Coverage:** + +- At least 85% line coverage for src/domain/templates/. +- At least 85% branch coverage for core rendering logic. + +### How to verify success + +1. Run the test suite: `cargo test --all` + - Expected: All tests pass (60+ tests total). + +2. Generate documentation: `cargo doc --no-deps --open` + - Expected: All public items are documented with examples. + +3. Verify determinism: + + ```bash + cargo test --test templates_snapshots -- --nocapture + ``` + + - Expected: All snapshot tests pass; no unexpected changes to + template content. + +4. Verify end-to-end: Create a simple binary that imports and uses the + public API: + + ```bash + cargo new --bin test_templates + # Add to Cargo.toml: reply_lib = { path = "../" } + # Write a main.rs that uses ReplyTemplate::GreetingMessage + cargo run + ``` + + - Expected: Binary compiles and prints a greeting message. + +5. Verify backward compatibility: `cargo test --all` in the parent + project. + - Expected: No existing tests fail; only new tests are added. + +## Idempotence and recovery + +All steps are idempotent and can be safely re-run: + +- Running `cargo test` multiple times produces the same results. +- Running `cargo fmt` multiple times does not change the code. +- Snapshot tests can be reviewed and approved by re-running with + `INSTA_REVIEW_MODE=overwrite`. +- If a test fails, run it in isolation: + `cargo test --test templates_unit test_name -- --nocapture`. + +If a milestone fails mid-way (e.g., a test fails), fix the issue and +re-run the stage: + +1. Identify the failing test or validation step. +2. Fix the code or test. +3. Re-run the stage from the beginning. +4. Commit the fix with a descriptive message. + +No rollback is needed; changes are incremental and safe. + +## Interfaces and dependencies + +### Public interfaces (to be created) + +**Module:** `crate::domain::templates` + +**Types:** + +- `ReplyTemplate` (enum): All default template variants. + - Variant examples: `GreetingMessage`, `ErrorResponse`, + `ConfirmationMessage`. + - Methods: `render(&self, context: &TemplateContext) -> String`, + `metadata(&self) -> TemplateMetadata`, `list_all() -> + Vec`. + +- `TemplateContext` (struct): Holds context variables for rendering. + - Methods: `new() -> Self`, `set(&mut self, key: &str, value: &str)`. + - Internal: Uses `BTreeMap` for deterministic + ordering. + +- `TemplateMetadata` (struct): Describes a template. + - Fields: `id: &'static str`, `name: &'static str`, `description: + &'static str`, `required_variables: &'static [&'static str]`. + +- `TemplateError` (enum): Template-related errors. + - Variants: `MissingVariable(String)`, `InvalidTemplate(String)`. + +**Re-exports in `crate::lib.rs`:** + +```rust +pub use crate::domain::templates::{ + ReplyTemplate, TemplateContext, TemplateMetadata, TemplateError +}; +``` + +### Dependencies + +**Build dependencies:** + +- `rstest` (dev-dependency): For parameterized unit tests. +- `proptest` (dev-dependency): For property-based tests. +- `insta` (dev-dependency): For snapshot tests. +- `pretty_assertions` (dev-dependency): For clear assertion messages. +- `googletest` (dev-dependency): For rich test assertions. + +**Runtime dependencies:** + +- None (core implementation uses only std library). + +**Optional/future:** + +- `serde` (optional feature): For template serialization (not in + initial scope). +- `askama` or similar (optional feature): For advanced template syntax + (not in initial scope). + +## Artifacts and notes + +### Summary of changes + +1. **New files:** + - `src/domain/templates/mod.rs` (~200 LOC): Public API types and + rendering logic. + - `src/domain/templates/presets.rs` (~300 LOC): Default template + content and constants. + - `src/domain/templates/errors.rs` (~50 LOC): Error types. + - `tests/templates_unit.rs` (~200 LOC): Unit tests. + - `tests/templates_bdd.rs` (~150 LOC): BDD scenarios. + - `tests/templates_snapshots.rs` (~100 LOC): Snapshot tests. + - `tests/templates_integration.rs` (~150 LOC): End-to-end tests. + - `docs/adr/adr-005-public-template-api.md` (~300 LOC): Architecture + decision record. + +2. **Updated files:** + - `src/lib.rs` (~5 LOC): Add public re-exports. + - `docs/users-guide.md`: Add section on using templates (~200 LOC). + - `docs/developers-guide.md`: Add implementation details (~150 LOC). + - `.gitignore`: Exclude snapshot backups (if needed). + +3. **Total new code (excluding tests and docs):** ~500 LOC. +4. **Total test code:** ~600 LOC. +5. **Total documentation:** ~650 LOC. + +### Example: Using the public template API + +```rust +use reply_lib::domain::templates::{ReplyTemplate, TemplateContext}; + +fn main() { + for template in ReplyTemplate::list_all() { + println!("Template: {}", template.metadata().name); + } + + let mut context = TemplateContext::new(); + context.set("name", "Alice"); + context.set("date", "2026-06-18"); + + let message = ReplyTemplate::GreetingMessage.render(&context); + println!("{}", message); +} +``` + +--- + +## Revision note + +(To be updated as the plan is revised during implementation.) diff --git a/docs/execplans/4-2-2-safe-host-mounted-workspaces.md b/docs/execplans/4-2-2-safe-host-mounted-workspaces.md new file mode 100644 index 00000000..b18ba472 --- /dev/null +++ b/docs/execplans/4-2-2-safe-host-mounted-workspaces.md @@ -0,0 +1,1324 @@ +# 4.2.2 Implement Safe Host-Mounted Workspaces + +This ExecPlan is a living document. The sections `Constraints`, +`Tolerances`, `Risks`, `Progress`, `Surprises & Discoveries`, +`Decision Log`, and `Outcomes & Retrospective` must be kept up to date +as work proceeds. + +Status: APPROVED + +## Purpose / big picture + +This milestone implements safe host-mounted workspaces—the mechanism by +which operators can mount host directories into container sessions while +maintaining strict boundaries against symlink escapes, unauthorized path +access, and privilege escalation vectors. + +After this change, users will be able to configure host-mounted +workspaces with confidence that all mount paths are canonicalized, +mounts are confined to allowlisted roots, write permissions are +validated, the threat model is documented, and forbidden paths are +rejected even under adversarial conditions. + +Success is observable when: + +1. Running `cargo test` in `tests/workspace_mounts_tests.rs` shows ≥40 tests + passing with ≥90% coverage +2. BDD scenarios in `tests/bdd/workspace_mounts.feature` all pass +3. Documentation in `docs/users-guide.md` and `docs/developers-guide.md` + describes configuration and threat model +4. `make check-fmt`, `make lint`, and `make test` all pass without errors +5. A PR against main is created with all gates passing + +## Constraints + +Hard invariants that must hold throughout implementation. + +- **No public API breaking changes**: Existing container launch signatures must + remain stable. New validation functions are internal to workspace_mounts + module. +- **No new external crate dependencies**: Use only Rust std::fs, std::path, and + existing project dependencies (thiserror for error handling, tracing for + logging). +- **Rootless Podman compatibility**: All validation must work correctly in + rootless namespaces without requiring uid 0. +- **Backward compatibility**: Existing workspace configurations must continue to + work; validation is additive. +- **Zero symlinks in validated paths**: After canonicalization, no symlinks may + exist in any component of the validated path. +- **Domain logic has no I/O**: All filesystem operations pass through + FilesystemPort trait; domain validator is pure logic, all I/O happens in + adapters. +- **Hexagonal architecture maintained**: Dependency graph must point inward; + ports defined at domain boundary; adapters implement ports, never call each + other. +- **All errors are domain-owned**: WorkspaceMountError enum is exhaustive; + infrastructure errors converted to domain errors at adapter boundaries. + +## Tolerances (exception triggers) + +Thresholds that trigger escalation when breached. + +- **Scope**: ≤20 files modified, ≤3000 net lines of code added. If exceeded, + stop and present impact analysis. +- **Dependencies**: Any new external crate must be escalated. Standard library + only, unless project already uses a crate (e.g., thiserror, tracing). +- **Interface changes**: If any public API signature in existing modules must + change, stop and escalate with rationale. +- **Test coverage**: Domain logic must reach ≥90% line coverage and ≥90% branch + coverage. If coverage gaps exist after Phase 5, stop and investigate. +- **Build/test failures**: If make test, make lint, or make check-fmt fails + after implementation, stop and fix before proceeding. +- **Threat model disagreement**: If during implementation the threat model + interpretation differs materially from the documented 7 attack vectors, + escalate with evidence. +- **Time**: If any single phase takes >6 hours, document progress and reassess + plan feasibility. + +## Risks + +Known uncertainties that might affect the plan. + +- **TOCTOU race: symlink added after validation but before mount** + Severity: High | Likelihood: Medium | Mitigation: Minimize validation + window; use `canonicalize()` + re-check symlinks immediately before mount; + document assumption that operator controls allowlist roots + +- **Permission validation unreliable in rootless: UID/GID remapping** + Severity: High | Likelihood: High | Mitigation: Add + RootlessPermissionAdapter that uses `libc::geteuid()` and + `libc::getegid()` to validate namespace context; write integration tests + with `podman unshare` + +- **Symlink cycles cause infinite loop during depth traversal** + Severity: Medium | Likelihood: Low | Mitigation: Set hard limit of 40 + components per path; detect cycles explicitly in + OsFilesystemAdapter.has_symlinks() + +- **Allowlist prefix attacks: /tmp/workspaces_evil matches /tmp/workspaces** + Severity: High | Likelihood: Low | Mitigation: Validate component + boundaries, not just string prefix; use `path.starts_with(root) && + (path.len() == root.len() || path[root.len()] == b'/')` pattern + +- **Mount flag escapes via remount/shared flags** + Severity: Medium | Likelihood: Low | Mitigation: Future enhancement; + document in roadmap; for now assume container runtime enforces safe flags + +- **Test suite insufficient**: Coverage tools may miss branches + Severity: Medium | Likelihood: Medium | Mitigation: Use `cargo tarpaulin + --out Html` for detailed branch coverage; manually inspect untested paths; + use property tests (proptest) for idempotence guarantees + +- **Integration test environment differences**: Tests pass locally but fail in + CI with different rootless setup + Severity: Medium | Likelihood: Medium | Mitigation: Test both as regular + user and with `podman unshare`; document platform assumptions; skip tests + that require rootless if not available + +## Progress + +Use checkboxes to track granular steps with timestamps. Each stage must +complete validation before proceeding. + +- [ ] **Stage 1: Threat model & architecture documentation** (Est. 2-3 hours) + - [ ] (TBD) Create docs/architecture/04-workspace-mounts-threat-model.md + - [ ] (TBD) Create docs/architecture/05-workspace-mounts-hexagonal-design.md + - [ ] (TBD) Create docs/adr/0003-workspace-mount-validation.md + - [ ] (TBD) Validation: Threat model is unambiguous, design approved + +- [ ] **Stage 2: Module structure & port traits** (Est. 1-2 hours) + - [ ] (TBD) Create src/workspace_mounts/ directory structure + - [ ] (TBD) Define all port traits (Filesystem, Permission, Config, + ThreatReporter) + - [ ] (TBD) Define value object stubs + - [ ] (TBD) Validation: `cargo check` passes + +- [ ] **Stage 3: Error type & value objects (Red-Green-Refactor)** (Est. 1-2 + hours) + - [ ] (TBD) Write Red: Unit tests for all 10 error variants + - [ ] (TBD) Write Green: Implement WorkspaceMountError, + ValidatedWorkspacePath, AllowedWorkspaceRoot + - [ ] (TBD) Refactor: Document and improve error messages + - [ ] (TBD) Validation: `cargo test --lib workspace_mounts` all pass + +- [ ] **Stage 4: FilesystemPort & OsFilesystemAdapter (Red-Green-Refactor)** + (Est. 2-3 hours) + - [ ] (TBD) Write Red: Unit & integration tests for symlink detection, + depth limit, cycles + - [ ] (TBD) Write Green: Implement OsFilesystemAdapter with canonicalize, + has_symlinks, metadata checks + - [ ] (TBD) Refactor: Extract helpers, add property tests + - [ ] (TBD) Validation: `cargo test --test integration` passes, ≥90% + coverage + +- [ ] **Stage 5: PermissionPort & RootlessPermissionAdapter** + **(Red-Green-Refactor)** (Est. 2-3 hours) + - [ ] (TBD) Write Red: Unit & integration tests for mode bits, UID/GID + checks + - [ ] (TBD) Write Green: Implement RootlessPermissionAdapter using libc + - [ ] (TBD) Add rootless integration tests + - [ ] (TBD) Validation: Works in both regular and rootless environments + +- [ ] **Stage 6: Domain Validator (Pure logic, Red-Green-Refactor)** (Est. 3-4 + hours) + - [ ] (TBD) Write Red: Unit tests for all attack vectors, property tests for idempotence + - [ ] (TBD) Write Green: Implement WorkspacePathValidator::validate() + - [ ] (TBD) Refactor: Extract pure helpers, document assumptions + - [ ] (TBD) Validation: ≥90% branch coverage, property tests pass + +- [ ] **Stage 7: Integration, BDD scenarios, & application service + (Red-Green-Refactor)** (Est. 3-4 hours) + - [ ] (TBD) Write Red: BDD scenarios in tests/bdd/workspace_mounts.feature + - [ ] (TBD) Write Green: Implement WorkspaceMountServiceImpl, BDD steps + - [ ] (TBD) Add snapshot tests, stress tests + - [ ] (TBD) Validation: All BDD scenarios pass, integration tests pass + +- [ ] **Stage 8: Documentation & deployment guide** (Est. 2-3 hours) + - [ ] (TBD) Update docs/users-guide.md with configuration and examples + - [ ] (TBD) Update docs/developers-guide.md with architecture and testing + - [ ] (TBD) Create docs/adr/0004-workspace-mount-validation-rationale.md + - [ ] (TBD) Validation: Documentation builds, examples are correct + +- [ ] **Stage 9: Quality gates & final validation** (Est. 2-3 hours) + - [ ] (TBD) Run make check-fmt, make lint, cargo test + - [ ] (TBD) Run cargo tarpaulin --out Html and verify ≥90% coverage + - [ ] (TBD) Create PR and request CodeRabbit review + - [ ] (TBD) Address review comments + - [ ] (TBD) Update roadmap marking 4.2.2 as COMPLETE + - [ ] (TBD) Validation: All gates pass, PR approved + +## Surprises & discoveries + +Unexpected findings during implementation. This section is updated as +work proceeds. + +(None yet; will be populated during implementation.) + +## Decision log + +Record every significant decision made while working on the plan. + +- Decision: Use Rust `std::fs::canonicalize()` as baseline + Rationale: Stable, audited. Avoids external crates unless stdlib + proves insufficient + Date: 2026-06-18 (planning phase) + +- Decision: Allowlist roots configured in YAML at startup + Rationale: Enforces operator control. Errors detected at startup, not + mount time + Date: 2026-06-18 (planning phase) + +- Decision: Permission validation uses `podman unshare` in rootless + Rationale: UID/GID remapping makes naive checks unreliable. Testing + in actual namespace is only reliable method + Date: 2026-06-18 (planning phase) + +(Additional decisions recorded during implementation.) + +## Outcomes & retrospective + +This section is populated at major milestones and at completion. + +(To be updated as work proceeds.) + +## Context and orientation + +### Current state + +Podbot is a container orchestration tool organized around composing +container launches from reusable request/plan primitives. The codebase +currently lacks specialized workspace mount validation, which is a gap for +safe host-mounted workspace support. + +**Related existing infrastructure**: + +- `src/config.rs` — Configuration loading and validation +- `src/container.rs` — Container launch and execution +- `Cargo.toml` — Dependencies (thiserror for errors, tracing for logging likely + available) +- Tests framework: cargo test (unit and integration) + +**Current limitations**: + +- No canonicalization of mount paths +- No allowlist enforcement +- No validation of mount permissions +- No threat model documentation +- Risk: Operators could accidentally mount dangerous paths (symlinks, parent + dirs, read-only dirs) + +### Project structure + +The Podbot codebase uses a modular approach with clear separation between +config, domain logic, and container execution. + +### Implementation approach: Hexagonal architecture + +This task uses **hexagonal architecture** (ports & adapters) to maintain clean +separation: + +- **Domain** (`src/workspace_mounts/domain/`) — Pure business logic, no I/O +- **Ports** (`src/workspace_mounts/domain/ports/`) — Abstract interfaces (what + domain requires from infrastructure) +- **Adapters** (`src/workspace_mounts/adapters/`) — Concrete implementations + (filesystem I/O, permission checks, config loading) +- **Application Service** (`src/workspace_mounts/lib.rs`) — Orchestrates + domain + adapters for callers + +This design ensures: + +- ✓ Domain logic is testable without I/O (use mocks) +- ✓ Adapters are easy to replace or extend (e.g., add SELinux checks later) +- ✓ All dependencies point inward (domain ← adapters) +- ✓ Errors are domain-owned (not infrastructure-specific) + +### Key files to create + +**Domain logic** (no I/O): + +- `src/workspace_mounts/domain/errors.rs` — WorkspaceMountError enum (10 + variants) +- `src/workspace_mounts/domain/model.rs` — Value objects: + ValidatedWorkspacePath, AllowedWorkspaceRoot, WorkspaceConfig +- `src/workspace_mounts/domain/validator.rs` — WorkspacePathValidator (pure + logic) +- `src/workspace_mounts/domain/ports/filesystem.rs` — FilesystemPort trait +- `src/workspace_mounts/domain/ports/permission.rs` — PermissionPort trait +- `src/workspace_mounts/domain/ports/config.rs` — WorkspaceConfigPort trait +- `src/workspace_mounts/domain/ports/threat_reporter.rs` — ThreatReportPort + trait + +**Adapters** (with I/O): + +- `src/workspace_mounts/adapters/os_filesystem.rs` — OsFilesystemAdapter + (std::fs wrapper) +- `src/workspace_mounts/adapters/permission.rs` — RootlessPermissionAdapter + (libc wrapper) +- `src/workspace_mounts/adapters/yaml_config.rs` — YamlConfigAdapter (YAML + config loader) +- `src/workspace_mounts/adapters/logging.rs` — LoggingThreatReporter (tracing + integration) + +**Application service** (boundary): + +- `src/workspace_mounts/lib.rs` — WorkspaceMountServiceImpl (orchestrates + domain + adapters) + +**Tests**: + +- `tests/workspace_mounts_tests.rs` — Unit tests with mocks +- `tests/workspace_mounts_integration_tests.rs` — Integration tests with real + filesystem +- `tests/bdd/workspace_mounts.feature` — Behavior-driven scenarios +- `tests/bdd/workspace_mounts_steps.rs` — BDD step implementations + +**Documentation**: + +- `docs/architecture/04-workspace-mounts-threat-model.md` — Security threat + model +- `docs/architecture/05-workspace-mounts-hexagonal-design.md` — Architecture + diagram & design +- `docs/adr/0003-workspace-mount-validation.md` — Architecture decision record +- `docs/users-guide.md` — Updated with configuration section +- `docs/developers-guide.md` — Updated with testing & architecture guidance + +## Plan of work + +Implementation proceeds through seven stages with explicit Red-Green-Refactor +validation at each domain logic milestone. All code changes are small, +testable, and committed after each stage passes quality gates. + +### Stage 1: Threat model & architecture documentation + +**Objective**: Design the domain model, ports, and adapters before writing +code. + +**Work**: + +1. Create `docs/architecture/04-workspace-mounts-threat-model.md` + documenting: + - All 7 attack vectors (symlink escape, TOCTOU, path traversal, allowlist + prefix bypass, permission escalation, rootless UID/GID mismatch, symlink + cycles) + - Prevention mechanisms for each + - Explicit threat boundaries (what we control vs. what we delegate) + - Assumptions (operator correctness, POSIX filesystem, atomic mounts) +2. Create `docs/architecture/05-workspace-mounts-hexagonal-design.md` with: + - Domain model (WorkspaceMountError, ValidatedWorkspacePath, + AllowedWorkspaceRoot, WorkspaceConfig) + - Ports (FilesystemPort, PermissionPort, WorkspaceConfigPort, + ThreatReportPort) + - Adapters (OsFilesystemAdapter, RootlessPermissionAdapter, + YamlConfigAdapter, LoggingThreatReporter) + - Hexagonal diagram +3. Create `docs/adr/0003-workspace-mount-validation.md` recording + architecture decisions + +**Validation**: Threat model is unambiguous; hexagonal diagram is reviewed; +no implementation has begun. + +**Estimated effort**: 2-3 hours + +### Stage 2: Module structure & port traits + +**Objective**: Create the module skeleton and port trait definitions. + +**Work**: + +1. Create new module `src/workspace_mounts/` directory with: + - `mod.rs` — exports public API + - `domain/errors.rs` — WorkspaceMountError enum (10 variants) + - `domain/model.rs` — Value objects (ValidatedWorkspacePath, + AllowedWorkspaceRoot, WorkspaceConfig) + - `domain/validator.rs` — Pure-logic domain service (WorkspacePathValidator) + - `domain/ports/filesystem.rs` — FilesystemPort trait + - `domain/ports/permission.rs` — PermissionPort trait + - `domain/ports/config.rs` — WorkspaceConfigPort trait + - `domain/ports/threat_reporter.rs` — ThreatReportPort trait + - `adapters/os_filesystem.rs` — OsFilesystemAdapter + - `adapters/permission.rs` — RootlessPermissionAdapter + - `adapters/yaml_config.rs` — YamlConfigAdapter + - `adapters/logging.rs` — LoggingThreatReporter + - `lib.rs` or public types in `mod.rs` — WorkspaceMountServiceImpl + (application service) + +2. Write all trait signatures and struct definitions (no implementation bodies + yet) + +**Validation**: `cargo check` passes; no missing trait methods; module structure +matches hexagonal design. + +**Estimated effort**: 1-2 hours + +### Stage 3: Error type & value objects (Red-Green-Refactor) + +**Objective**: Define exhaustive error type and invariant-enforcing value +objects. + +**Red**: Write unit tests in `tests/workspace_mounts_tests.rs` that: + +- Construct each of the 10 WorkspaceMountError variants +- Verify ValidatedWorkspacePath can only be created by validator +- Verify AllowedWorkspaceRoot rejects invalid paths at construction + +**Green**: Implement: + +- WorkspaceMountError with `#[derive(Debug, Clone, PartialEq)]` and + thiserror::Error +- ValidatedWorkspacePath as newtype with private constructor; `pub fn + new_unchecked()` for validator only +- AllowedWorkspaceRoot with validation at construction (path exists, is + directory, is absolute) +- WorkspaceConfig as aggregate root (holds list of AllowedWorkspaceRoot) + +**Refactor**: Ensure error Display messages are actionable; document each +variant with example. + +**Validation**: `cargo test --lib workspace_mounts` passes; 100% error type +coverage; all error messages are tested. + +**Estimated effort**: 1-2 hours + +### Stage 4: FilesystemPort & OsFilesystemAdapter (Red-Green-Refactor) + +**Objective**: Implement abstracted filesystem operations. + +**Red**: Write unit tests with mocks: + +- `test_canonicalize_rejects_symlinks_in_path` +- `test_canonicalize_resolves_dotdot` +- `test_has_symlinks_detects_all_components` +- `test_has_symlinks_respects_depth_limit` +- `test_has_symlinks_detects_cycles` + +Write integration tests with real filesystem (using tempfile): + +- Create actual symlink, verify detection +- Create symlink cycle, verify depth limit prevents DoS +- Create deeply nested symlinks, verify limit respected + +**Green**: Implement: + +- FilesystemPort trait with methods: `canonicalize()`, `has_symlinks()`, + `read_metadata_nofollow()`, `exists()` +- OsFilesystemAdapter using std::fs, std::path, fs::read_link() +- Depth limit constant (40 components) +- Cycle detection via visited set + +**Refactor**: Extract helper functions; document assumptions; add property tests +for idempotence. + +**Validation**: `cargo test --test integration filesystem` passes; `cargo +tarpaulin` shows ≥90% branch coverage for adapter. + +**Estimated effort**: 2-3 hours + +### Stage 5: PermissionPort & RootlessPermissionAdapter (Red-Green-Refactor) + +**Objective**: Implement permission validation compatible with rootless +containers. + +**Red**: Write unit tests with mocks: + +- `test_is_writable_checks_mode_bits` +- `test_is_writable_rejects_read_only` +- `test_container_ids_returns_euid_egid` +- `test_rootless_context_validation_succeeds_in_namespace` + +Write integration tests: + +- Set path mode to 0o444, verify writable check rejects +- Run in `podman unshare` namespace, verify UID/GID checks work + +**Green**: Implement: + +- PermissionPort trait with methods: `is_writable()`, `container_ids()`, + `check_security_context()` +- RootlessPermissionAdapter using libc::geteuid(), libc::getegid() +- Mode bit checking: `(stat.st_mode & 0o200) != 0` + +**Refactor**: Document namespace assumptions; add error context showing actual +vs. expected UID. + +**Validation**: `cargo test --test integration permission` passes; works in both +regular and rootless environments. + +**Estimated effort**: 2-3 hours + +### Stage 6: Domain Validator (Pure logic, Red-Green-Refactor) + +**Objective**: Implement pure-logic domain validator without I/O. + +**Red**: Write comprehensive unit tests with mocks: + +- Three-stage validation passes for valid paths +- Symlink escape attempt rejected at canonicalize step +- Path traversal (/../..) rejected at canonicalize step +- Allowlist prefix attack (/tmp/workspaces_evil vs /tmp/workspaces) rejected +- Permission check catches read-only directories +- TOCTOU windows are minimized (re-check after external operations) + +Property tests: + +- `validate(validate(path))` returns same result (idempotence) +- Allowlist member check is consistent (same path always passes/fails) + +**Green**: Implement WorkspacePathValidator::validate(): + +```rust +pub fn validate(&self, path: &Path) -> Result { + // Step 1: Canonicalize (rejects symlinks) + let canonical = self.filesystem.canonicalize(path)?; + + // Step 2: Enforce allowlist (checks component boundaries) + self.check_in_allowlist(&canonical)?; + + // Step 3: Validate permissions (checks writable) + self.validate_permissions(&canonical)?; + + // Step 4: Report success + self.threat_reporter.report_mount_approved(&canonical); + Ok(ValidatedWorkspacePath::new_unchecked(canonical)) +} +``` + +**Refactor**: Extract pure helper methods; document assumptions; add error +context. + +**Validation**: `cargo test --lib validator` passes; ≥90% branch coverage; +property tests pass. + +**Estimated effort**: 3-4 hours + +### Stage 7: Integration, BDD scenarios, application service + +(Red-Green-Refactor) + +**Objective**: Integrate all components; write E2E tests; expose public API. + +**Red**: Write BDD scenarios in `tests/bdd/workspace_mounts.feature`: + +```gherkin +Feature: Safe host-mounted workspaces + Scenario: Valid workspace path is validated successfully + Given an allowed root "/tmp/workspaces" + And a valid workspace path "/tmp/workspaces/job-123" + When I validate the workspace mount + Then the mount should be approved + + Scenario: Symlink escape attempt is rejected + Given an allowed root "/tmp/workspaces" + And a symlink "/tmp/workspaces/link" pointing to "/etc" + When I validate the workspace mount for "/tmp/workspaces/link" + Then the mount should be rejected with reason "symlink detected" + + Scenario: Path outside allowlist is rejected + Given allowed roots ["/tmp/workspaces"] + And a valid path "/home/attacker/workspace" + When I validate the workspace mount + Then the mount should be rejected with reason "not in allowlist" +``` + +Run `cargo test --test bdd` and watch all scenarios fail. + +**Green**: Implement WorkspaceMountServiceImpl: + +```rust +pub struct WorkspaceMountServiceImpl { + validator: Arc, +} + +impl WorkspaceMountServiceImpl { + pub fn new(config, filesystem, permission, threat_reporter) -> Self { ... } + pub fn validate_mount(&self, path: &Path) -> Result { ... } + pub fn validate_mounts(&self, paths: &[PathBuf]) + -> Result, Vec<(PathBuf, WorkspaceMountError)>> { ... } +} +``` + +Write BDD step implementations (using cucumber or similar). + +Add snapshot tests showing error message formatting for each error variant. + +Add stress tests: validate 1000 paths with various attacks. + +**Refactor**: Ensure error messages are consistent; add structured logging via +ThreatReportPort; document composition pattern. + +**Validation**: `cargo test --test bdd` all scenarios pass; `cargo test --test +integration` all integration tests pass; `cargo test --lib` all unit tests pass. + +**Estimated effort**: 3-4 hours + +### Stage 8: Documentation & deployment guide + +**Objective**: Document configuration, threat model, and deployment steps. + +**Work**: + +1. Update `docs/users-guide.md` with: + - Configuration schema (YAML example) + - Allowed roots setup and validation + - Error messages and troubleshooting + - Examples of valid and invalid paths + +2. Update `docs/developers-guide.md` with: + - Module architecture (domain, adapters) + - How to add new adapters (e.g., SELinux context port) + - Testing guidelines (unit, integration, BDD) + - Security review checklist + +3. Create `docs/adr/0004-workspace-mount-validation-rationale.md` with + decisions and trade-offs + +**Validation**: Documentation builds without errors; examples are tested +(copy-paste works). + +**Estimated effort**: 2-3 hours + +### Stage 9: Quality gates & final validation + +**Objective**: Run all checks and verify production readiness. + +**Work**: + +1. Run `make check-fmt` — all workspace_mounts code is formatted +2. Run `make lint` — no clippy warnings +3. Run `cargo test` — all tests pass +4. Run `cargo tarpaulin --out Html --timeout 300` — verify ≥90% coverage +5. Run `make check-docs` if available — documentation builds +6. Create draft PR and request CodeRabbit review +7. Address any review comments +8. Mark task as COMPLETE in roadmap + +**Validation**: All gates pass; PR is ready for merge. + +**Estimated effort**: 2-3 hours (including review turnaround) + +## Validation and acceptance + +### Acceptance criteria (all must be satisfied) + +1. **Test coverage**: `cargo tarpaulin --out Html` shows ≥90% line coverage and + ≥90% branch coverage for all workspace_mounts code +2. **Unit tests pass**: `cargo test --lib workspace_mounts` runs with no + failures; tests exercise all 10 error variants +3. **Integration tests pass**: `cargo test --test integration workspace_mounts` + passes; tests use real filesystem and tempfile +4. **BDD scenarios pass**: `cargo test --test bdd workspace_mounts` passes; all + Gherkin scenarios in `tests/bdd/workspace_mounts.feature` pass +5. **Property tests pass**: Idempotence properties validated; + `validate(validate(path))` always returns same result +6. **Quality gates**: + - `make check-fmt` passes (all workspace_mounts code is formatted) + - `make lint` passes (no clippy warnings) + - `cargo test` (full suite) passes with no failures +7. **Documentation complete**: + - `docs/users-guide.md` includes configuration section with + examples + - `docs/developers-guide.md` describes module architecture and testing + strategy + - `docs/architecture/04-workspace-mounts-threat-model.md` documents all + threat vectors + - `docs/adr/0003-workspace-mount-validation.md` records design decisions +8. **Configuration integration**: Podbot config loading accepts + workspace_mounts configuration and validates at startup +9. **Rootless compatibility**: Tests pass in both regular and rootless (via + `podman unshare`) environments +10. **PR created**: Branch pushed; PR created against main with all checks + passing; CodeRabbit review completed + +### Quality method: Red-Green-Refactor + +For each domain logic component (error type, validator, adapters), follow this +discipline: + +**Red phase**: + +```bash +# Write failing test(s) that specify desired behaviour +# For unit tests, use mocks; for integration, use real filesystem +cargo test --lib workspace_mounts -- --nocapture +# Expected: test fails with "assertion failed" or "not implemented" +``` + +**Green phase**: + +```bash +# Implement minimal code to make test pass +# Prioritize correctness over optimization +cargo test --lib workspace_mounts -- --nocapture +# Expected: test passes +``` + +**Refactor phase**: + +```bash +# Clean up implementation without changing behaviour +# Extract pure helpers, improve error messages, add documentation +cargo test --lib workspace_mounts -- --nocapture +# Expected: test still passes; code is cleaner +``` + +### Quality method: Coverage verification + +After implementation of each stage, measure coverage: + +```bash +# Install tarpaulin if not present +cargo install cargo-tarpaulin + +# Run coverage on workspace_mounts module +cargo tarpaulin --out Html --timeout 300 --exclude-files 'tests/*' +# Expected: ≥90% line coverage, ≥90% branch coverage for workspace_mounts/ + +# Review HTML report to identify untested paths +open tarpaulin-report.html # or your browser +``` + +If coverage is <90%, investigate untested branches and add tests before +proceeding. + +### Quality method: Integration testing with real filesystem + +For filesystem and permission adapters, test with real filesystem: + +```bash +# Create temp directory for tests +TEMP_DIR=$(mktemp -d) +cd $TEMP_DIR + +# Create test fixtures +mkdir -p workspaces/valid +ln -s /etc symlink_to_etc +touch readonly.txt && chmod 444 readonly.txt + +# Run integration tests +cd /path/to/podbot +cargo test --test integration workspace_mounts -- --nocapture + +# Cleanup +rm -rf $TEMP_DIR +``` + +### Quality method: Rootless testing + +For permission validation, test in rootless namespace: + +```bash +# Verify podman is available +podman --version + +# Run tests in rootless namespace (if available) +podman unshare cargo test --test integration permission -- --nocapture + +# Expected: Permission checks succeed in namespace context +# Expected: UID/GID in namespace context match expected values +``` + +### Quality method: BDD scenario validation + +```bash +# Run BDD scenarios +cargo test --test bdd workspace_mounts -- --nocapture + +# Expected output includes: +# Scenario: Valid workspace path is validated successfully ... PASSED +# Scenario: Symlink escape attempt is rejected ... PASSED +# Scenario: Path outside allowlist is rejected ... PASSED +# All scenarios pass with correct rejection reasons +``` + +### Expected outputs + +**After Stage 3 (error type)**: + +```text +running 10 tests +test domain::errors::tests::error_variant_symlink_detected ... ok +test domain::errors::tests::error_variant_not_in_allowlist ... ok +... (8 more variants) +test result: ok. 10 passed; 0 failed +``` + +**After Stage 4 (filesystem adapter)**: + +```text +running 25 tests (15 unit + 10 integration) +... filesystem canonicalization tests +... symlink detection tests +... depth limit tests +test result: ok. 25 passed; 0 failed +coverage: 92% (24/26 branches) +``` + +**After Stage 6 (domain validator)**: + +```text +running 30 tests (20 unit + 10 property) +test domain::validator::tests::validate_accepts_allowlisted_path ... ok +test domain::validator::tests::validate_rejects_symlink_escape ... ok +test domain::validator::properties::prop_idempotent ... ok +... (27 more) +test result: ok. 30 passed; 0 failed +``` + +**After Stage 7 (full integration)**: + +```text +running 50 tests (20 unit + 15 integration + 15 BDD) +test tests::workspace_mounts_tests::* ... ok +test tests::workspace_mounts_integration_tests::* ... ok +running feature: workspace_mounts +Scenario: Valid workspace path is validated successfully ... PASSED +Scenario: Symlink escape attempt is rejected ... PASSED +... (13 more scenarios) +test result: ok. 50 passed; 0 failed +coverage: 92% (lines), 91% (branches) +``` + +**Final quality gates**: + +```bash +$ make check-fmt +All files formatted correctly ✓ + +$ make lint +No clippy warnings ✓ + +$ cargo test +... 150+ tests pass (unit + integration + BDD + all other tests) ✓ + +$ cargo tarpaulin +workspace_mounts: 92% coverage ✓ + +$ cargo build --release +Build succeeded ✓ +``` + +## Threat model summary + +The threat model identifies and mitigates seven major attack vectors: + +| # | Attack Vector | Prevention | +| --- | --- | --- | +| 1 | Symlink Escape | Canonicalize + reject | +| 2 | TOCTOU Race | Minimize window | +| 3 | Symlink Cycle DoS | Depth limit (40) | +| 4 | Path Traversal | Canonicalize `..` | +| 5 | Allowlist Prefix Bypass | Boundary check | +| 6 | Permission Escalation | Writable check | +| 7 | Rootless UID/GID Mismatch | Namespace ID check | + +**Out of scope** (delegated to container runtime): + +- Kernel bugs (runc/crun) — Mitigation: keep patched +- Mount flag attacks (remount, shared) — Future enhancement +- Capability escalation — Delegated to container runtime LSM/AppArmor + +**Assumptions**: + +1. Operator configures allowlist roots correctly (no symlinks in config itself) +2. Filesystem is standard POSIX (ext4, tmpfs, overlayfs) +3. Podman/container runtime enforces safe mount flags +4. UID/GID mapping in rootless mode is correct + +## Test strategy + +### Test pyramid (40+ tests total) + +**Unit Tests (25+)** — Domain logic with mocks, no I/O + +- Error enum: 10 tests (one per variant) +- Value objects: 5 tests (construction, invariants) +- Canonicalization logic: 5 tests (symlinks, traversal, depth) +- Allowlist: 3 tests (membership, prefix bypass, component boundaries) +- Permission: 2 tests (mode bits, idempotence) +- Domain validator: 5 tests (integration of three checks) + +**Property Tests (6+)** — Formal verification of invariants + +- Idempotence: `validate(validate(x)) == validate(x)` +- Allowlist consistency: `is_member(x, roots)` always same result +- Canonicalization symmetry: `canonicalize(canonicalize(x)) == canonicalize(x)` +- Error stability: Same invalid path always produces same error +- Depth limit: Paths >40 components always rejected +- Symlink coverage: All symlink scenarios detected + +**Integration Tests (10+)** — Real filesystem, no mocks + +- Symlink creation and detection +- Symlink cycle creation and handling +- Depth limit enforcement with nested symlinks +- Permission bits validation (read-only files) +- Real config file loading +- Temporary directory cleanup + +**BDD Scenarios (8+)** — End-to-end behavior specification + +- Valid workspace path accepted +- Symlink escape attempt rejected +- Path outside allowlist rejected +- Read-only directory rejected +- Configuration loading and validation +- Error messages are actionable + +## Stress Tests (1+) + +- Validate 1000 paths with various attacks +- Measure canonicalization performance + +### Tools & commands + +```bash +# Run all tests +cargo test + +# Unit tests only (fast, no I/O) +cargo test --lib workspace_mounts + +# Integration tests (slower, real filesystem) +cargo test --test integration workspace_mounts + +# BDD scenarios +cargo test --test bdd workspace_mounts + +# Property tests (proptest crate) +cargo test --lib workspace_mounts -- --test-threads=1 proptest + +# Coverage measurement +cargo tarpaulin --out Html --timeout 300 + +# Coverage targeting specific module +cargo tarpaulin --packages podbot --lib workspace_mounts --out Html +``` + +### Coverage targets + +- **Line coverage**: ≥90% for workspace_mounts module +- **Branch coverage**: ≥90% for workspace_mounts module +- **Error path coverage**: All 10 error variants tested +- **Untested paths**: None (coverage gaps require investigation and new tests) + +## Interfaces and dependencies + +### New module: `src/workspace_mounts/` + +Public API exported from `src/workspace_mounts/lib.rs`: + +```rust +// Error type (10 variants, all recoverable) +pub enum WorkspaceMountError { ... } + +// Value objects (invariant-enforced) +pub struct ValidatedWorkspacePath { ... } +pub struct AllowedWorkspaceRoot { ... } +pub struct WorkspaceConfig { ... } + +// Application service (what consumers use) +pub struct WorkspaceMountServiceImpl { ... } + +impl WorkspaceMountServiceImpl { + pub fn new(config, filesystem, permission, threat_reporter) -> Self { ... } + pub fn validate_mount(&self, path: &Path) -> Result { ... } + pub fn validate_mounts(&self, paths: &[PathBuf]) + -> Result, Vec<(PathBuf, WorkspaceMountError)>> { ... } +} +``` + +### Internal module structure + +```text +src/workspace_mounts/ +├── mod.rs # Public exports +├── lib.rs # Application service +├── domain/ +│ ├── errors.rs # WorkspaceMountError (10 variants) +│ ├── model.rs # Value objects +│ ├── validator.rs # WorkspacePathValidator (pure logic) +│ ├── ports/ +│ │ ├── filesystem.rs # FilesystemPort trait +│ │ ├── permission.rs # PermissionPort trait +│ │ ├── config.rs # WorkspaceConfigPort trait +│ │ └── threat_reporter.rs # ThreatReportPort trait +│ └── tests/ +│ ├── errors_tests.rs +│ ├── model_tests.rs +│ ├── validator_tests.rs +│ └── properties_tests.rs +│ +└── adapters/ + ├── os_filesystem.rs # OsFilesystemAdapter (std::fs) + ├── permission.rs # RootlessPermissionAdapter (libc) + ├── yaml_config.rs # YamlConfigAdapter (config loading) + └── logging.rs # LoggingThreatReporter (tracing) +``` + +### Extension to existing modules + +**`src/config.rs`** — Add configuration support: + +```rust +pub struct PodobotConfig { + // ... existing fields + pub workspace_mounts: Option, +} + +pub struct WorkspaceMountConfig { + pub allowed_roots: Vec, + // future: mount_flags, validation_mode, etc. +} +``` + +### Dependencies + +**Use from Rust standard library**: + +- `std::fs` — Filesystem operations +- `std::path` — Path handling +- `std::fs::read_link()` — Symlink detection +- `libc` — POSIX UID/GID checks (likely already dependency) + +**Use from existing Podbot dependencies**: + +- `thiserror` — Error enum derivation +- `tracing` — Structured logging + +**No new external crates** without escalation. + +**Testing-only crates** (already available): + +- `proptest` — Property testing +- `tempfile` — Temporary test directories +- `mockall` — Mock trait implementations (or `double` if available) +- `insta` — Snapshot testing +- Cucumber/BDD framework if available, else write custom BDD runner + +### Type signatures (reference for implementation) + +```rust +// Domain errors +#[derive(Debug, Clone, Error)] +pub enum WorkspaceMountError { + #[error("symlink detected at {path}: {component:?}")] + SymlinkDetected { + path: PathBuf, + component: Option, + }, + + #[error("path not in allowlist: {path}\nallowed: {allowed_roots:?}")] + NotInAllowlist { + path: PathBuf, + allowed_roots: Vec, + }, + + // ... 8 more variants +} + +// Ports (abstractions) +pub trait FilesystemPort { + fn canonicalize(&self, path: &Path) -> Result; + fn has_symlinks(&self, path: &Path) -> Result; + fn read_metadata_nofollow( + &self, + path: &Path, + ) -> Result; + fn exists(&self, path: &Path) -> Result; +} + +pub trait PermissionPort { + fn is_writable(&self, path: &Path) -> Result<()>; + fn container_ids(&self) -> Result<(u32, u32)>; + fn check_security_context( + &self, + path: &Path, + ) -> Result<()>; +} + +// Domain service (pure logic) +pub struct WorkspacePathValidator { + config: WorkspaceConfig, + filesystem: Arc, + permission: Arc, + threat_reporter: Arc, +} + +impl WorkspacePathValidator { + pub fn validate( + &self, + path: &Path, + ) -> Result; +} + +// Application service (what callers use) +pub struct WorkspaceMountServiceImpl { + validator: Arc, +} + +impl WorkspaceMountServiceImpl { + pub fn validate_mount( + &self, + path: &Path, + ) -> Result; + pub fn validate_mounts( + &self, + paths: &[PathBuf], + ) -> Result, Vec<(PathBuf, WorkspaceMountError)>>; +} +``` + +## Integration with other tasks + +This task depends on and integrates with: + +- **Task 1.4.1 (Container Launch)**: The WorkspaceMountServiceImpl validates + all workspace paths before container command construction. Canonical paths + are passed to bind_mount(). + +- **Task 2.2.5 (MCP Integration)**: The same service is used by MCP workspace + mount protocol handler. Ensures consistent validation across all launch + entry points. + +**Data flow**: + +```text +HTTP/CLI/MCP request + ↓ +WorkspaceMountServiceImpl::validate_mount() + ↓ +WorkspacePathValidator (pure domain logic) + ├─→ Canonicalize (FilesystemPort) + ├─→ Check allowlist (pure logic) + └─→ Validate permissions (PermissionPort) + ↓ +ValidatedWorkspacePath (newtype, invariant-enforced) + ↓ +Container launch (Task 1.4.1) or MCP response (Task 2.2.5) +``` + +Both tasks use identical validation, preventing validation bypass via +different launch paths. + +## Concrete steps for implementation + +### Setup and environment + +```bash +# Ensure you're on the correct branch +git branch --show-current +# Expected: 4-2-2-safe-host-mounted-workspaces + +# Verify Rust toolchain +rustc --version +cargo --version +# Expected: stable or recent nightly + +# Install tarpaulin for coverage (if not present) +cargo install cargo-tarpaulin + +# Create temp directory for test fixtures +export TEST_FIXTURES_DIR=$(mktemp -d) +echo "Test fixtures will be in: $TEST_FIXTURES_DIR" +``` + +### Stage-by-stage implementation + +Each stage proceeds through Red-Green-Refactor, followed by validation: + +```bash +# After completing each stage, run: +cargo check # Compilation check +cargo test --lib # Unit tests +cargo test --test integration # Integration tests +make check-fmt # Format check +make lint # Lint check + +# If all pass, proceed to next stage. +# If any fails, stop and fix before proceeding. +``` + +### Coverage measurement at each stage + +```bash +# After Stage 4 (filesystem adapter) and beyond: +cargo tarpaulin --out Html --timeout 300 \ + --exclude-files 'tests/*' +# Expected: ≥85% coverage (will increase as more stages complete) + +# Final coverage check (Stage 9): +cargo tarpaulin --out Html --timeout 300 \ + --exclude-files 'tests/*' +# Expected: ≥90% line coverage, ≥90% branch coverage +``` + +### PR creation (final step) + +```bash +# After all stages complete and quality gates pass: +git log --oneline -10 +# Verify commits are descriptive (one per stage) + +git push -u origin 4-2-2-safe-host-mounted-workspaces + +# Create PR (use gh or GitHub web interface) +gh pr create \ + --title "Implement safe host-mounted workspaces (task 4.2.2)" \ + --body "Closes # + +## Summary +- Implement WorkspaceMountService with hexagonal architecture +- Add validation for canonicalization, allowlist, permissions +- Add 40+ tests with ≥90% coverage +- Document threat model and configuration + +## Validation +- All quality gates pass (fmt, lint, test) +- Coverage ≥90% line and branch +- BDD scenarios pass +- Documentation updated" + +# Expected: PR created and CI checks run +``` + +## Idempotence and recovery + +All stages are idempotent: + +- **Tests are idempotent**: Run `cargo test` multiple times, expect same + results +- **Code changes are additive**: Each stage adds new modules or functions; + no destructive refactoring of existing code +- **Configuration is immutable at runtime**: Once loaded, config is never + modified +- **Test fixtures are cleaned up**: Use `tempfile::TempDir` to auto-cleanup + +**If a stage fails partway through**: + +1. Identify which step failed (read error message) +2. Fix the issue (e.g., add missing import, fix logic) +3. Rerun the stage's validation from the beginning +4. Do not skip steps or skip commits + +**If a test fails repeatedly** (3+ attempts): + +1. Stop and escalate (per Tolerances) +2. Document the issue in `Decision Log` +3. Propose a path forward with trade-offs + +## Artifacts and notes + +### Key design decisions + +1. **Hexagonal architecture** — Isolates domain logic from I/O; enables + testing without infrastructure; simplifies future enhancements (e.g., + SELinux checks) + +2. **Canonicalization always rejects symlinks** — Stricter security posture; + no symlinks in validated paths ever + +3. **Allowlist is configuration-based** — Operators control explicitly via + YAML; validated at startup (fail-fast) + +4. **Permission validation is strict** — Read-only directories are rejected; + prevents mount-time failures + +5. **Error type is exhaustive** — 10 variants, each with actionable context; + all errors are domain-owned + +6. **Three-stage validation** — Canonicalize → Allowlist → Permissions; each + stage is independent and testable + +7. **Shared service across launch paths** — Same WorkspaceMountServiceImpl + used by HTTP, CLI, MCP; prevents validation bypass + +### Example configuration + +```yaml +podbot: + workspace_mounts: + allowed_roots: + - /tmp/workspaces + - /home/ci/work + - /var/lib/podbot/volumes +``` + +### Example error flow + +## Invalid path: symlink escape + +```rust +// Input: /tmp/workspaces/link → /etc +validate_mount("/tmp/workspaces/link") + ↓ +canonicalize() → detected symlink at "link" + ↓ +Error::SymlinkDetected { path, component: Some("link") } + ↓ +ThreatReporter::report_mount_rejection() + ↓ +Response to operator: "Mount rejected: symlink detected at +/tmp/workspaces/link (component: link)" +``` + +## Invalid path: outside allowlist + +```rust +// Input: /home/attacker/workspace +// Config: allowed_roots: [/tmp/workspaces] +validate_mount("/home/attacker/workspace") + ↓ +canonicalize() → Ok(/home/attacker/workspace) + ↓ +check_in_allowlist() → not a member + ↓ +Error::NotInAllowlist { path, allowed_roots: [/tmp/workspaces] } + ↓ +Response: "Mount rejected: /home/attacker/workspace not in allowlist +[/tmp/workspaces]" +``` + +--- + +## Revision note + +(None yet; to be updated if the plan is revised during implementation.) diff --git a/docs/execplans/6-2-1-write-quickstart-guide.md b/docs/execplans/6-2-1-write-quickstart-guide.md new file mode 100644 index 00000000..4eaf40cf --- /dev/null +++ b/docs/execplans/6-2-1-write-quickstart-guide.md @@ -0,0 +1,990 @@ +# Write Quickstart Guide for shared-actions (6.2.1) + +This ExecPlan (execution plan) is a living document. The sections `Constraints`, +`Tolerances`, `Risks`, `Progress`, `Surprises & Discoveries`, `Decision Log`, +and `Outcomes & Retrospective` must be kept up to date as work proceeds. + +## Status + +DRAFT + +## Purpose / big picture + +The shared-actions repository contains 17 reusable GitHub Actions focused on +Rust and Python build, test, package, and release automation. Today, new users +face a steep discovery curve: the README provides only a bare table listing +actions with no guidance on which ones to use, how they fit together, or how to +compose them into working workflows. Individual action READMEs exist but are +isolated reference docs showing single-action usage patterns, not realistic +end-to-end pipelines. + +This ExecPlan will deliver `docs/quickstart.md`, a scenario-driven onboarding +guide that bridges the gap between the action table and deep reference +documentation. After following this guide, a user new to shared-actions will be +able to: + +1. Understand what these actions do and which ones solve their problem + (within 5 minutes of reading) +2. See a complete, runnable workflow example for their use case (Rust + build+release, Python PyPI publish, coverage measurement, or Dependabot + auto-merge) +3. Know how to customize the example for their project and where to find + deeper docs +4. Understand common pitfalls and security considerations when using GitHub + Actions + +The guide will be validated by running all provided YAML examples through the +project's existing action-validator tool and composability smoke tests to +ensure they produce expected outputs and work end-to-end. + +## Constraints + +Hard invariants that must hold throughout implementation. + +- **Scope:** The guide must remain a quickstart (max 1,200 lines including + YAML examples). Complex topics (Cargo configuration, nFPM templating, + platform-specific cross-compilation) link to existing deep-dive + documentation rather than being explained in full. + +- **Content source:** Every action mentioned in the guide already exists in + this repository. No new actions are created as part of this task. All + examples derive from or are validated against existing action tests and + documented workflows (`.github/workflows/ci.yml`, test fixtures). + +- **Linking over duplication:** When explaining concepts (composite actions, + reusable workflows, caching, GitHub token permissions), link to existing + reference docs (AGENTS.md, developers-guide.md, individual action READMEs) + rather than duplicating explanation. + +- **Single source of truth:** The master table of actions remains in the root + README.md. The quickstart guide adds narrative and examples but does not + replace that table. + +- **Action versioning:** Examples use published major-version tags for remote + usage (e.g., `owner/shared-actions/.github/actions/setup-rust@v1`) and + local-path syntax for development (`./.github/actions/setup-rust`). + Examples must match the current semantic versioning strategy documented in + AGENTS.md. + +- **Platform coverage:** Examples must be valid YAML and runnable on at least + ubuntu-latest. Platform-specific gotchas (Windows GUI tools, macOS + cross-compilation, Linux systemd services) must be explicitly noted or + omitted. + +- **No breaking changes:** Implementing this guide must not require changes to + existing actions, workflows, or tooling. If such changes are discovered to + be necessary, stop and escalate. + +## Tolerances (exception triggers) + +Thresholds that trigger escalation when breached. + +- **Scope creep:** If the guide grows beyond 1,200 lines (excluding code + examples) or requires adding more than 4 complete YAML scenario examples, + stop and escalate. + +- **Example validation failure:** If any provided YAML example fails + `action-validator` or would fail on a representative CI runner + (ubuntu-latest), stop with error logs and escalate. + +- **Design inconsistencies:** If writing examples reveals that following the + guide's recommended action sequence would fail at runtime on a specific OS + or with specific inputs (even if no documentation contradiction exists), + file an issue with a minimal reproduction and escalate. Do not work around + the inconsistency with undocumented workarounds. + +- **Documentation conflicts:** If two action READMEs contradict each other + (e.g., conflicting input defaults, incompatible output formats), file a + GitHub issue and escalate rather than proceeding. + +- **Ambiguous target audience:** If it becomes unclear whether the guide + should prioritize Rust users, Python users, or equal coverage, escalate. + The current decision is: Rust-primary, with Python as a secondary example, + since shared-actions is primarily a Rust tooling library. + +- **Missing testing infrastructure:** If the guide requires inventing new test + fixtures, CI workflows, or smoke-test infrastructure not already present in + the repository, escalate. + +- **Scope definition:** The quickstart will include full, runnable YAML + examples for exactly these actions: (1) `setup-rust`, (2) + `rust-build-release`, (3) one platform packager (`linux-packages` + preferred), (4) `release-to-pypi-uv`. All other actions receive 1-line + descriptions + links to individual READMEs. Deviations from this list + trigger scope-creep escalation. + +## Risks + +Known uncertainties that might affect the plan. + +- **Risk:** Examples become stale as actions evolve. + Severity: high + Likelihood: high + Mitigation: Cross-reference each example against corresponding `action.yml` + files. Add comments pinpointing exact input versions. Include a validation + date in the guide (e.g., "validated against shared-actions v1.2.3, + 2026-06-18"). Establish a quarterly review cadence to refresh examples + against the latest action defaults. + +- **Risk:** Action interdependencies are more complex than documented. + Severity: high + Likelihood: high + Mitigation: Before writing examples, create a dependency matrix + distinguishing: (a) always-required (e.g., `setup-rust` before compilation), + (b) conditionally-required (e.g., `linux-packages` only for release builds), + (c) optional (e.g., coverage generation). Document this matrix upfront in + the guide to set correct expectations. + +- **Risk:** Examples work on ubuntu-latest but fail on macOS or Windows + runners. + Severity: medium + Likelihood: medium + Mitigation: Validate Rust examples on ubuntu-latest and macos-latest if + feasible. For platform-specific actions (e.g., `windows-package`), test + locally or note the limitation. Clearly annotate platform-specific sections + in the guide. + +- **Risk:** First-time users are confused by the difference between actions + (reusable steps) and reusable workflows (full workflow files). + Severity: medium + Likelihood: medium + Mitigation: Include a brief comparison section early in the guide. Link to + existing doc: docs/composite-actions-vs-full-workflows.md. + +- **Risk:** Users skip the quickstart and go directly to individual action + READMEs, missing the narrative context. + Severity: low + Likelihood: medium + Mitigation: Add a prominent "Start here" link in the root README.md pointing + to the new quickstart. + +- **Risk:** Glossary terms are unfamiliar to GitHub Actions newcomers. + Severity: low + Likelihood: medium + Mitigation: Include a "Glossary" section at the end of the guide. Define: + composite action, reusable workflow, runner, sccache, nextest, nFPM, + artefacts, staging. + +## Progress + +Use a list with checkboxes to summarise granular steps. Every stopping point +must be documented here. + +- [ ] (TBD) Phase 1: Research & scope clarification + - [ ] Confirm Rust-primary vs equal coverage decision + - [ ] Create dependency matrix for actions + - [ ] Finalize file path and location (docs/quickstart.md vs + docs/getting-started.md) + - [ ] Define maintenance owner and update process + +- [ ] (TBD) Phase 2: Outline & structure + - [ ] Draft the guide outline section by section + - [ ] Select 3–4 representative scenarios (Rust build, Python release, + coverage, auto-merge) + - [ ] Create a spreadsheet of action categories and dependencies + - [ ] Identify which existing docs to link vs which content to inline + +- [ ] (TBD) Phase 3: Content creation + - [ ] Write hero section and prerequisites + - [ ] Write "What's Inside" section with category overview + - [ ] Write Scenario A: Rust binary build + release (with full YAML) + - [ ] Write Scenario B: Python package to PyPI (with full YAML) + - [ ] Write Scenario C: Coverage measurement (with example) + - [ ] Write Scenario D: Dependabot auto-merge (with link to workflow) + - [ ] Write "Understanding Action Dependencies" section + - [ ] Write "Common Patterns" section + - [ ] Write "Troubleshooting" section + - [ ] Write "Next Steps" section + - [ ] Add "Glossary" section + +- [ ] (TBD) Phase 4: Validation & testing + - [ ] Run all YAML examples through `make lint` (action-validator) + - [ ] Verify all internal doc links (to AGENTS.md, developers-guide.md, etc.) + - [ ] Create minimal smoke-test workflows for each scenario + - [ ] Validate on ubuntu-latest runner + - [ ] Peer review for clarity and accuracy + - [ ] Ensure consistent tone and voice + +- [ ] (TBD) Phase 5: Integration & merge + - [ ] Update root README.md to link to quickstart + - [ ] Add quickstart link to AGENTS.md "Next Steps" + - [ ] Add maintenance notes to execplan Outcomes section + - [ ] Create draft PR with comment linking to this execplan + - [ ] Incorporate review feedback + - [ ] Merge with all CI gates passing + +## Surprises & discoveries + +Unexpected findings during implementation that were not anticipated as risks. To +be updated as work proceeds. + +(No surprises yet; implementation has not begun.) + +## Decision log + +Record every significant decision made while working on the plan. + +- **Decision:** Prioritize Rust-primary content with Python as secondary +example. + Rationale: The action catalog contains 16 Rust-specific actions and only 1 + Python-specific action (release-to-pypi-uv). A guide treating both equally + would create asymmetric expectations. Python users can reference external PyPI + docs; Rust users have minimal alternatives within shared-actions. + Date/Author: 2026-06-18 / execplan research phase. + +- **Decision:** Limit examples to 4 actions (setup-rust, rust-build-release, +linux-packages, release-to-pypi-uv). + Rationale: Prevents scope creep. All other actions receive mention + link. A + user seeking deeper guidance on a specific action reads that action's + individual README. + Date/Author: 2026-06-18 / expert review gap #3. + +- **Decision:** Create dependency matrix before writing examples. + Rationale: Addresses expert review gap #1. Actions have conditional + dependencies (setup-rust is optional if user already has Rust; linux-packages + is only needed for release builds). A matrix clarifies when each action is + required vs optional. + Date/Author: 2026-06-18 / expert review gap #1. + +- **Decision:** Assign explicit maintenance owner. + Rationale: Addresses expert review gap #6. Guide must be kept current as + actions evolve. Owner is responsible for quarterly review and updating + examples when action defaults change materially. + Date/Author: 2026-06-18 / expert review gap #6. + +- **Decision:** Use `docs/quickstart.md` as the filename. + Rationale: Consistent with GitHub's documentation convention. Discoverable + from docs/ directory and linkable from README.md. + Date/Author: 2026-06-18 / expert review gap #6. + +(Additional decisions to be recorded as implementation proceeds.) + +## Outcomes & retrospective + +To be updated at major milestones or completion. Compare result against purpose +and note lessons learned. + +(Outcomes to be recorded upon completion.) + +## Context and orientation + +The shared-actions repository is a GitHub-hosted collection of 17 reusable +GitHub Actions and 1 reusable workflow, focused on automating build, test, +package, and release workflows for Rust and Python projects. + +### Key files + +- **README.md** (root): Master table of 17 actions with major version and + path. This is the entry point users see. +- **docs/developers-guide.md**: Internal architecture for contributors. Covers + concurrency assumptions, venv management, caching strategies. +- **docs/composite-actions-vs-full-workflows.md**: Explains when to use actions + vs workflows. +- **docs/generate-coverage-design.md**: Deep dive into coverage tooling + (slipcover, pytest-xdist). +- **docs/rust-build-release-pipeline.md**: Detailed guide to Rust + build/package/release sequencing. +- **AGENTS.md**: Foundational constraints, tool resolution, CI/CD strategies. +- **.github/actions/**: Directory containing 17 action subdirectories, each + with action.yml and README.md. +- **.github/workflows/**: CI workflows (ci.yml, dependabot-automerge.yml) that + exemplify action usage. +- **Makefile**: Contains targets like `fmt`, `lint`, `test`. The `lint` target + runs action-validator. + +### Key actions for this guide + +1. **setup-rust**: Installs Rust toolchain, cargo-binstall, optional DB dev + libraries, cross-compilers. +2. **rust-build-release**: Builds Rust binaries with configurable features and + targets. Outputs staging paths for packagers. +3. **linux-packages**: Creates .deb and .rpm packages using nFPM from staged + binaries. +4. **release-to-pypi-uv**: Publishes Python packages to PyPI using uv. +5. **generate-coverage**: Measures code coverage with slipcover (Python) or + cargo-tarpaulin (Rust). +6. **dependabot-automerge**: Reusable workflow for automatically merging + Dependabot PRs. + +### Current documentation gaps + +- No narrative guide showing how to compose actions into a realistic + end-to-end workflow. +- No clear decision tree for users ("Am I building a Rust binary? A Python + package? Both?"). +- Examples exist in ci.yml but are not surfaced or explained to new users. +- Individual action READMEs are reference docs, not tutorials. + +### Target audience + +- First-time users of GitHub Actions or shared-actions specifically (estimated + background: basic GitHub familiarity). +- Maintainers of mono-repositories needing templated CI/CD. +- DevOps engineers building composite workflows. +- Project owners evaluating whether shared-actions fits their pipeline. + +### Success criteria (observable) + +1. A new user can read the quickstart in ≤10 minutes and understand: what these +actions do, which ones solve their problem, how to use a chosen action in their +workflow. +2. At least one complete Rust example (setup-rust → rust-build-release → +linux-packages → release asset upload) is provided and validated. +3. At least one Python example (release-to-pypi-uv) is provided and validated. +4. All YAML examples pass `make lint` (action-validator). +5. All internal doc links are correct. +6. A peer reviewer unfamiliar with shared-actions can follow the guide without +external context. + +## Plan of work + +The work consists of five sequential phases. Each phase has defined go/no-go +validation before proceeding to the next. + +### Phase 1: Research & Scope Clarification (2 hours) + +**Goal:** Resolve the 6 gaps identified in expert review before writing begins. + +### Phase 1 Steps + +1. Create a dependency matrix distinguishing: + - Always-required actions (e.g., setup-rust before rust-build-release) + - Conditionally-required actions (e.g., linux-packages only for release + builds with packaging) + - Optional actions (e.g., generate-coverage can run independently) + + Document this matrix in a plaintext file (e.g., + `docs/execplans/6-2-1-dependency-matrix.txt`) so it's visible during + implementation. + +2. Finalize the 4-action scope: setup-rust, rust-build-release, linux-packages, +release-to-pypi-uv. Confirm this list aligns with the author's intent. If not, +escalate. + +3. Decide on file location and hierarchy. File will be `docs/quickstart.md`. +Confirm this path and update this execplan if different. + +4. Identify maintenance owner (a person or team responsible for quarterly review +and updates). Document in execplan Outcomes section once identified. + +5. Review the four scenarios and their representative use cases: + - Scenario A (Rust build + release): for maintainers releasing Rust binaries + - Scenario B (Python PyPI): for maintainers publishing Python packages + - Scenario C (Coverage): for any project measuring code coverage + - Scenario D (Dependabot auto-merge): for any project using Dependabot + +### Phase 1 Go/no-go gate + +- All 5 items above are completed and documented. +- Author confirms the scope and 4-action list. +- If any item is ambiguous, escalate for clarification before proceeding to +Phase 2. + +**Artifacts:** dependency-matrix.txt, updated execplan Decision Log. + +--- + +### Phase 2: Outline & Structure (2 hours) + +**Goal:** Draft the guide structure without writing full prose, ensuring +coverage and flow. + +### Phase 2 Steps + +1. Create a detailed outline of all sections: + - Hero section (1-liner, audience, key features) + - Prerequisites (GitHub Actions knowledge, repo setup) + - What's Inside (category overview, action selector) + - Scenario A outline (Rust build + release) + - Scenario B outline (Python PyPI) + - Scenario C outline (Coverage) + - Scenario D outline (Dependabot auto-merge) + - Understanding Action Dependencies (matrix summary) + - Common Patterns (matrix builds, conditional steps, caching) + - Troubleshooting (3–5 common issues) + - Next Steps (links to deeper docs) + - Glossary (5–8 key terms) + +2. For each scenario, identify: + - The exact actions to show (which of the 4 core actions apply) + - The YAML file path and snippet to provide + - Links to existing docs that provide deeper explanation + - Expected output or success criteria + +3. Create a spreadsheet mapping: + - Use case → Actions involved → Individual README links → Where in guide + +4. Identify all internal doc links (AGENTS.md, developers-guide.md, individual +action READMEs, etc.) and verify they exist. + +5. Draft section headers and sub-headers in a skeleton .md file. + +### Phase 2 Go/no-go gate + +- Outline is complete with all sections clearly named. +- All internal doc links are verified to exist. +- The 4-action scope is reflected consistently in the outline. +- Author reviews outline and confirms structure. + +**Artifacts:** Skeleton `docs/quickstart.md` with headers only; spreadsheet of +action mappings. + +--- + +### Phase 3: Content Creation (4–5 hours) + +**Goal:** Write all narrative and code sections, validating YAML syntax as you +go. + +### Phase 3 Steps + +1. **Hero & Prerequisites section** (30 min): + - Write 1-line description: "Reusable GitHub Actions for Rust and Python + projects." + - Explain who this guide is for. + - List 3–4 key features (build, test, package, release). + - Explain prerequisites (GitHub Actions familiarity, .github/workflows/ + directory structure). + +2. **What's Inside section** (30 min): + - Provide a category-based overview (Build, Test, Package, Release, + Utilities). + - Create a small table mapping categories to representative actions. + - Add a decision tree: "Are you building Rust, Python, or both? → Go to + Scenario A/B/C/D." + +3. **Scenario A: Rust Binary Build + Release** (60 min): + - Write prose explaining the use case. + - Provide a minimal workflow.yml YAML snippet (checkout → setup-rust → + rust-build-release → linux-packages → upload-release-assets). + - Annotate the YAML with comments explaining each step. + - Show typical input values and how to customize. + - Explain the output (staging directory, release assets). + - Link to individual action READMEs for deeper customization. + - **Validation**: Run this YAML through `make lint` (action-validator) and + verify it passes. + +4. **Scenario B: Python Package to PyPI** (30 min): + - Write prose explaining the use case. + - Provide a minimal workflow.yml snippet (checkout → release-to-pypi-uv). + - Annotate with comments. + - Explain how to set up PyPI credentials/token. + - Link to action README and external PyPI docs. + - **Validation**: Run YAML through `make lint` and verify it passes. + +5. **Scenario C: Coverage Measurement** (30 min): + - Explain the use case (measure code coverage across Rust and Python). + - Provide a workflow snippet showing generate-coverage with lang=rust and + lang=python in separate jobs. + - Link to docs/generate-coverage-design.md for deep dive. + - **Validation**: Run YAML through `make lint`. + +6. **Scenario D: Dependabot Auto-Merge** (20 min): + - Explain the reusable workflow pattern. + - Show how to reference the workflow in a user's repository. + - Link to docs/composite-actions-vs-full-workflows.md and the + dependabot-automerge.yml workflow file. + - No new YAML needed (workflow already exists in repo). + +7. **Understanding Action Dependencies section** (20 min): + - Present the dependency matrix in prose form (not a table; use bullet + points). + - Example: "setup-rust must run before rust-build-release, but is optional if + your runner already has Rust installed." + - Explain sequencing rules and when actions can run in parallel. + - Reference the matrix created in Phase 1. + +8. **Common Patterns section** (20 min): + - Show: local vs remote usage (`./.github/actions/setup-rust` vs + `owner/shared-actions/.github/actions/setup-rust@v1`). + - Show: matrix builds across platforms. + - Show: conditional step execution. + - Link to developers-guide.md for caching details. + - **Note:** Keep these as brief examples; deeper guidance lives in linked + docs. + +9. **Troubleshooting section** (20 min): + - Provide 3–5 common issues and resolutions: + 1. "My build failed in the action but worked locally" → Explain runner + differences, link to act documentation. + 2. "How do I debug an action step?" → Explain GitHub Actions debug logs and + ACTIONS_STEP_DEBUG. + 3. "Can I use just one of these actions without the others?" → Yes, they're + composable; show cherry-pick example. + 4. "My Dependabot PR didn't auto-merge" → Check permissions, token, branch + protection rules. + 5. "I need to package for Windows, but windows-package is v0" → Explain + versioning strategy, point to action README. + - Link to local-validation-of-github-actions-with-act-and-pytest.md. + +10. **Next Steps section** (15 min): + - Link to full AGENTS.md for comprehensive action list. + - Link to developers-guide.md for architecture deep-dives. + - Link to individual action READMEs. + - Link to GitHub Actions official documentation. + - Contributing guidelines. + +11. **Glossary section** (15 min): + - Define 5–8 terms: + - Composite action + - Reusable workflow + - Runner + - sccache + - nextest + - nFPM + - Artefacts (staging) + - Slipcover + +### Phase 3 Go/no-go gate + +- All prose sections are written. +- All YAML examples pass `make lint` (action-validator). +- All internal doc links are verified. +- No section is missing. + +**Artifacts:** Complete `docs/quickstart.md` file. + +--- + +### Phase 4: Validation & Testing (2–3 hours) + +**Goal:** Ensure all examples are correct, testable, and discoverable. + +### Phase 4 Steps + +1. **YAML Validation** (30 min): + - Run `make lint` from repository root. + - Verify all YAML snippets in the guide pass action-validator. + - Fix any indentation, syntax, or reference errors. + +2. **Link Verification** (30 min): + - Verify all internal links (to AGENTS.md, developers-guide.md, individual + action READMEs, etc.) exist and are correct. + - Use grep or a link checker to catch broken paths. + - Example: `grep -n "AGENTS.md" docs/quickstart.md` should return valid file + references. + +3. **Composability Smoke Tests** (60 min): + - For Scenario A (Rust build + release): Create a minimal test workflow that + runs setup-rust → rust-build-release with a fixture Rust project. + - For Scenario B (Python PyPI): Create a test workflow that runs + release-to-pypi-uv with a mock PyPI endpoint or dry-run flag (if supported). + - For Scenario C (Coverage): Create a test workflow that runs + generate-coverage with both lang=rust and lang=python. + - Run each test workflow locally with `act` or in CI. + - Verify that actions produce expected outputs and no runtime errors occur. + +4. **Platform Coverage** (30 min): + - Validate Rust examples on ubuntu-latest (minimum). + - If feasible, also test on macos-latest. + - Document any platform-specific gotchas in the guide. + +5. **Peer Review** (30 min): + - Have a maintainer unfamiliar with the actions read the guide. + - Gather feedback on clarity, completeness, and tone. + - Flag any sections that are confusing or require external context. + - Incorporate feedback. + +### Phase 4 Go/no-go gate + +- All YAML passes `make lint`. +- All links are correct. +- Smoke tests run without errors on ubuntu-latest. +- Peer review is complete with no major clarity issues. + +**Artifacts:** Validated `docs/quickstart.md`, smoke-test workflow files, peer +review notes. + +--- + +### Phase 5: Integration & Merge (1 hour) + +**Goal:** Finalize the guide, update root README, and merge to main branch. + +### Phase 5 Steps + +1. **Update root README.md** (15 min): + - Add a link in the README.md hero or "Next Steps" section pointing to + docs/quickstart.md. + - Example: "New to shared-actions? Start with the [Quickstart + Guide](docs/quickstart.md)." + +2. **Update AGENTS.md** (5 min): + - Add a link to docs/quickstart.md in the "Getting Started" or + "Documentation" section. + +3. **Document maintenance** (10 min): + - Update this execplan's Outcomes section with: + - Name and GitHub handle of maintenance owner. + - Quarterly review schedule. + - Process for updating examples when action defaults change. + +4. **Create draft PR** (15 min): + - Commit changes (docs/quickstart.md, updated README.md, updated AGENTS.md). + - Push to branch `6-2-1-write-quickstart-guide`. + - Create a draft PR with title: "(6.2.1) Write Quickstart Guide for + shared-actions". + - In PR description, link to this execplan and summarize what was delivered. + - Include a "References" section with the lody session link. + +5. **Merge with gates passing** (15 min): + - Ensure all CI/CD gates pass (lint, typecheck, tests if any). + - Mark PR as ready for review. + - Await approval and merge to main. + +### Phase 5 Go/no-go gate + +- All phases 1–4 are complete and validated. +- PR is open and all CI gates pass. + +**Artifacts:** Merged docs/quickstart.md, updated README.md and AGENTS.md, +merged PR. + +## Concrete steps + +**Working directory:** `/tmp/lody-title-agent` (the shared-actions repository). + +### Phase 1: Research & Scope Clarification + +```bash +# Step 1: Create dependency matrix +cat > docs/execplans/6-2-1-dependency-matrix.txt << 'EOF' +ACTION DEPENDENCY MATRIX + +Always-required (must run before dependent actions): +- setup-rust → rust-build-release (setup-rust installs Rust; + rust-build-release requires it) + +Conditionally-required (needed only for specific workflows): +- rust-build-release → linux-packages (only if you want to create .deb/.rpm packages) +- rust-build-release → macos-package (only if you want to create macOS .pkg) +- rust-build-release → windows-package (only if you want to create Windows .msi/.zip) +- Any build action → generate-coverage (only if measuring coverage) +- generate-coverage → ... (optional; produces artifacts but doesn't + affect other actions) + +Optional (can run independently): +- setup-rust (if runner already has Rust, can skip) +- release-to-pypi-uv (independent Python workflow; does not require any Rust actions) +- dependabot-automerge (separate reusable workflow; does not depend on build actions) + +Platform-specific: +- linux-packages requires ubuntu-latest runner +- macos-package requires macos-latest runner +- windows-package requires windows-latest runner +EOF + +# Verify it was created +ls -l docs/execplans/6-2-1-dependency-matrix.txt +echo "Matrix created." +``` + +Expected output: + +```text +-rw-r--r--. 1 leynos leynos 822 Jun 18 xx:xx docs/execplans/6-2-1-dependency-matrix.txt +Matrix created. +``` + +**Go/no-go:** Proceed to Phase 2 if all 5 items from Phase 1 steps are completed +and documented in this execplan's Decision Log. + +--- + +### Phase 2: Outline & Structure + +```bash +# Create skeleton docs/quickstart.md with headers only +cat > docs/quickstart.md << 'EOF' +# Quickstart Guide + +## Hero Section + +## Prerequisites + +## What's Inside + +### Action Categories + +### How to Choose Your Path + +## Scenarios + +### Scenario A: Build and Release a Rust Binary + +### Scenario B: Publish a Python Package to PyPI + +### Scenario C: Measure Code Coverage + +### Scenario D: Automate Dependabot Merges + +## Understanding Action Dependencies + +## Common Patterns + +## Troubleshooting + +## Next Steps + +## Glossary +EOF + +echo "Skeleton created at docs/quickstart.md" + +# Verify links to existing docs +ls -1 docs/{developers-guide,composite-actions,generate-coverage}*.md | head -5 +``` + +Expected output: + +```text +Skeleton created at docs/quickstart.md +docs/composite-actions-vs-full-workflows.md +docs/developers-guide.md +docs/generate-coverage-design.md +``` + +**Go/no-go:** Skeleton exists with all section headers. Outline is complete. +Author confirms structure before proceeding. + +--- + +### Phase 3: Content Creation + +This phase involves writing prose for each section. Example validation during +Scenario A: + +```bash +# After writing Scenario A YAML, extract and validate it +grep -A 50 "Scenario A:" docs/quickstart.md | grep -A 20 '```yaml' > /tmp/scenario-a.yml + +# Run through action-validator +make lint 2>&1 | tee /tmp/lint-output.txt | head -20 +``` + +Expected output if no errors: + +```text +[OK] All actions validated successfully. +``` + +If errors occur, fix YAML indentation and rerun until passing. + +--- + +### Phase 4: Validation & Testing + +```bash +# Verify all internal links exist +for link in AGENTS.md developers-guide.md \ + composite-actions-vs-full-workflows.md generate-coverage-design.md; do + if grep -q "$link" docs/quickstart.md; then + if [ -f "docs/$link" ] || [ -f "$link" ]; then + echo "✓ $link exists" + else + echo "✗ $link NOT FOUND" + fi + fi +done +``` + +Expected output: + +```text +✓ AGENTS.md exists +✓ developers-guide.md exists +✓ composite-actions-vs-full-workflows.md exists +✓ generate-coverage-design.md exists +``` + +--- + +### Phase 5: Integration & Merge + +```bash +# Add link to root README.md (in "Next Steps" or "Getting Started" section) +# This is a manual edit; update the appropriate section with: +# "New to shared-actions? Start with the [Quickstart Guide](docs/quickstart.md)." + +# Commit changes +git add docs/quickstart.md docs/README.md AGENTS.md +git commit -m "Add quickstart guide for shared-actions (6.2.1) + +Provides scenario-driven onboarding for new users, including: +- Rust binary build and release example +- Python PyPI publishing example +- Coverage measurement example +- Dependabot auto-merge walkthrough + +All examples are validated against action.yml files and pass action-validator." + +# Push and create PR +git push -u origin 6-2-1-write-quickstart-guide +``` + +Expected: Commit message, successful push, PR created. + +--- + +## Validation and acceptance + +### Observable behaviour + +After following the guide, a new user should be able to: + +1. **Understand the purpose:** Run through "What's Inside" section and correctly +identify that setup-rust + rust-build-release is the path for Rust binary +projects. + +2. **Run a complete example:** Copy the Scenario A YAML into their repository at +`.github/workflows/release.yml`, adjust project-specific fields (binary name, +target platforms), push to GitHub, and see a successful build and release +workflow execute. + +3. **Understand dependencies:** Read the "Understanding Action Dependencies" +section and predict which actions must run in sequence and which are optional. + +4. **Find deeper docs:** Locate links to individual action READMEs and +architectural deep-dives for topics they want to customize (e.g., Cargo +features, nFPM configuration). + +### Quality criteria + +### Content + +- [ ] Guide is between 600–1,200 lines (excluding code examples). +- [ ] Includes at least 4 distinct use-case scenarios (Rust, Python, coverage, + Dependabot). +- [ ] All YAML examples pass `make lint`. +- [ ] All internal doc links are correct and point to existing files. +- [ ] Glossary covers at least 5 key terms. + +### Examples + +- [ ] Each YAML example is copy-paste runnable (no manual edits beyond + project-specific fields like binary name). +- [ ] Examples are validated against corresponding action.yml files and + existing workflows (ci.yml). +- [ ] At least one Rust example and one Python example are present. +- [ ] Examples include realistic input values and comments explaining how to + customize. + +### Validation + +- [ ] `make lint` passes (no markdown or YAML errors). +- [ ] Peer review by a maintainer unfamiliar with shared-actions is complete + with no unresolved clarity issues. +- [ ] No broken internal or external links. +- [ ] Platform-specific notes (if any) are clearly marked. + +### Integration + +- [ ] Root README.md updated to link to quickstart. +- [ ] AGENTS.md updated to reference quickstart in "Getting Started" or + similar section. +- [ ] New file is discoverable (linked from README.md and AGENTS.md). +- [ ] All CI/CD gates pass with the new file added. + +### Commands to verify success + +```bash +# Verify file exists and has content +wc -l docs/quickstart.md +# Expected: > 600 lines + +# Verify YAML examples pass validation +make lint +# Expected: All checks pass + +# Verify links are correct +grep -o '\[.*\](.*\.md)' docs/quickstart.md | head -5 +# Expected: Links to AGENTS.md, developers-guide.md, etc. + +# Verify GitHub Actions syntax +grep -c '```yaml' docs/quickstart.md +# Expected: >= 3 (at least 3 YAML examples) +``` + +### Acceptance definition + +Delivery is complete when: + +1. `docs/quickstart.md` exists with 600–1,200 lines of content. +2. All YAML examples pass `make lint` without errors. +3. Peer review is complete and no major clarity issues remain. +4. Root README.md and AGENTS.md are updated with links to the quickstart. +5. All CI/CD gates pass. +6. A draft PR is open with a reference to this execplan and the lody session. + +## Idempotence and recovery + +All steps in this plan are idempotent. If a phase fails or is incomplete: + +- **Phase 1–2:** Delete and recreate the files (dependency-matrix.txt, skeleton +docs/quickstart.md). No external systems are affected. + +- **Phase 3:** Rewrite the prose or YAML sections. Validation in Phase 4 will +catch errors. + +- **Phase 4:** Re-run validation steps. Link checks and YAML validation are +re-runnable. + +- **Phase 5:** If commit or push fails, fix the issue and retry. If PR creation +fails, create manually. + +If a tolerance threshold is breached (e.g., YAML fails validation), stop +immediately and escalate rather than working around the error. + +## Artifacts and notes + +Key artifacts produced: + +1. **docs/quickstart.md** — The main deliverable. Scenario-driven guide with 4 +examples, ~800–1,000 lines. + +2. **docs/execplans/6-2-1-dependency-matrix.txt** — Clarity on which actions are +always-required vs optional. + +3. **Updated README.md** — Added link to quickstart. + +4. **Updated AGENTS.md** — Added reference in "Getting Started" section. + +5. **Draft PR** — (6.2.1) Write Quickstart Guide for shared-actions, linking to +this execplan. + +## Interfaces and dependencies + +### External dependencies + +- GitHub Actions: Users' workflows will `uses` the actions defined in + `.github/actions/`. +- Documentation: Links to existing docs (AGENTS.md, developers-guide.md, + individual action READMEs). + +No new interfaces or dependencies are created by this plan. + +### Validation tools + +- `make lint` — Runs action-validator to check YAML syntax. +- `act` (local execution tool) — Optional, for smoke testing workflows + locally before CI. +- `grep` / link checkers — For validating documentation links. + +--- + +## Status: DRAFT + +This ExecPlan is ready for review and approval. It addresses the 6 expert review +gaps and provides a clear roadmap for writing a scenario-driven quickstart guide +that reduces friction for new users of shared-actions while preserving deep +reference documentation for advanced customization. + +To proceed, the user must: + +1. Review this execplan and approve or request revisions. +2. Confirm the 4-action scope and Rust-primary positioning. +3. Identify the maintenance owner for quarterly reviews. +4. Approve proceeding to Phase 1. + +Upon approval, implementation will proceed milestone-by-milestone with clear +go/no-go gates at each phase. diff --git a/docs/execplans/auto-merge-on-unstable.md b/docs/execplans/auto-merge-on-unstable.md index 5428e749..ccc61fbe 100644 --- a/docs/execplans/auto-merge-on-unstable.md +++ b/docs/execplans/auto-merge-on-unstable.md @@ -1,8 +1,8 @@ # Enable Dependabot Auto-Merge When Merge State Is UNSTABLE -This execution plan (ExecPlan) is a living document. The sections `Constraints`, `Tolerances`, -`Risks`, `Progress`, `Surprises & Discoveries`, `Decision Log`, and -`Outcomes & Retrospective` must be kept up to date as work proceeds. +This execution plan (ExecPlan) is a living document. The sections `Constraints`, +`Tolerances`, `Risks`, `Progress`, `Surprises & Discoveries`, `Decision Log`, +and `Outcomes & Retrospective` must be kept up to date as work proceeds. Status: COMPLETE @@ -16,9 +16,10 @@ the workflow enables GitHub auto-merge earlier (while checks are pending or not yet green), and GitHub then performs the final merge only after branch protection and required checks are satisfied. -Success is observable in workflow logs: an eligible Dependabot pull request (PR) with -`automerge_merge_state=UNSTABLE` should now emit `automerge_status=enabled` -instead of `automerge_status=skipped` with `merge-state-unstable`. +Success is observable in workflow logs: an eligible Dependabot pull request +(PR) with `automerge_merge_state=UNSTABLE` should now emit +`automerge_status=enabled` instead of `automerge_status=skipped` with +`merge-state-unstable`. ## Constraints @@ -94,9 +95,9 @@ silently broadening scope. - Discovery: The current repository has no `PLANS.md`, so this ExecPlan is governed by the `execplans` skill format and `AGENTS.md` instructions. -- Discovery: Qdrant memory Model Context Protocol (MCP) tools (`qdrant-find`, `qdrant-store`) are not - exposed in this execution environment; no remote project-memory lookup was - possible from this session. +- Discovery: Qdrant memory Model Context Protocol (MCP) tools (`qdrant-find`, + `qdrant-store`) are not exposed in this execution environment; no remote + project-memory lookup was possible from this session. - Discovery: The existing unit suite already has a dedicated test asserting `UNSTABLE` is skipped (`merge_state_unstable_skips`), which will need to be inverted rather than adding entirely new harness plumbing. diff --git a/docs/execplans/dependabot-auto-merge.md b/docs/execplans/dependabot-auto-merge.md index aa161242..87e7d46d 100644 --- a/docs/execplans/dependabot-auto-merge.md +++ b/docs/execplans/dependabot-auto-merge.md @@ -73,8 +73,10 @@ local validation path via `pytest` + `act`. - [x] (2026-01-12 00:00Z) Add unit tests for the helper script. - [x] (2026-01-12 00:00Z) Add workflow integration test via `act` harness. - [x] (2026-01-12 00:00Z) Update documentation with usage guidance. -- [x] (2026-01-12 00:00Z) Reordered dry-run validation to require event payload before PR number resolution. -- [x] (2026-01-12 00:00Z) Adjusted linux-packages worktree repo-root test to accept `.git` files. +- [x] (2026-01-12 00:00Z) Reordered dry-run validation to require event + payload before PR number resolution. +- [x] (2026-01-12 00:00Z) Adjusted linux-packages worktree repo-root test to + accept `.git` files. - [x] (2026-01-12 00:00Z) Ran format, lint, typecheck, and test gates. - [x] (2026-01-12 00:00Z) Committed implementation changes. diff --git a/docs/execplans/refactor-validate-packages-modules.md b/docs/execplans/refactor-validate-packages-modules.md index e24666e9..9679a3e2 100644 --- a/docs/execplans/refactor-validate-packages-modules.md +++ b/docs/execplans/refactor-validate-packages-modules.md @@ -6,7 +6,6 @@ and `Outcomes & Retrospective` must be kept up to date as work proceeds. Status: COMPLETE - ## Purpose / big picture Improve cohesion and maintainability of the validate-linux-packages action by @@ -17,41 +16,54 @@ versus diagnostic formatting versus path validation. The public API remains unchanged, all tests pass without modification, and imports from validate_packages continue to work via re-exports. - ## Constraints Hard invariants that must hold throughout implementation: - Public API stability: all functions currently in `__all__` of + validate_packages.py must remain importable from validate_packages after the refactoring (via re-export). + - Test compatibility: all existing tests must pass without modification. Tests + import via `validate_packages_module` fixture which loads validate_packages.py dynamically. + - Backward compatibility: any code importing from validate_packages (including + validate_cli.py and tests) must continue working without changes. + - No functional changes: this is a pure refactoring; behaviour must remain + identical. + - Type safety: all type hints must remain valid; typecheck must pass. - The duplicate `_trim_output()` function must be resolved (two definitions at - lines 148 and 385 with different signatures). + lines 148 and 385 with different signatures). ## Tolerances (exception triggers) - Scope: if refactoring requires changes to more than 7 files (5 new modules + + validate_packages.py + one test discovery), stop and escalate. + - Interface: if any function in `__all__` changes signature or behaviour, stop + and escalate. + - Dependencies: if new external dependencies are required, stop and escalate. - Iterations: if tests fail after initial refactoring and fixing imports takes + more than 3 attempts, stop and escalate. -- Time: if any module extraction takes more than 30 minutes, stop and escalate. +- Time: if any module extraction takes more than 30 minutes, stop and escalate. ## Risks - Risk: The duplicate `_trim_output()` functions have different signatures (one + at line 148 with line_limit and char_limit parameters, one at line 385 without). The second definition overrides the first, so some call sites expecting the multi-line variant may not work correctly. @@ -61,18 +73,19 @@ Hard invariants that must hold throughout implementation: that preserves the intended behaviour for all callers. - Risk: Tests use dynamic module loading via fixture; imports from new modules + might not be properly discovered. Severity: low Likelihood: low Mitigation: Verify re-exports work correctly with the fixture-based loading. - Risk: Circular import dependencies may emerge when extracting modules. + Severity: medium Likelihood: low Mitigation: Keep imports unidirectional; validate_packages imports from the new modules, not vice versa. - ## Progress - [x] (2025-02-17) Audit `_trim_output()` duplicate and resolve signature conflict. @@ -87,10 +100,10 @@ Hard invariants that must hold throughout implementation: - [x] (2025-02-17) Run `make lint` and verify no errors. - [x] (2025-02-17) Run `make check-fmt` and verify formatting is correct. - ## Surprises & discoveries - Observation: The duplicate `_trim_output()` functions serve different purposes. + The first (line 148) preserves multi-line structure with line limits, while the second (line 385) normalises to a single line for logging. Both are needed. Evidence: Call sites at lines 174, 184, 191, 283, 286 use multi-line variant @@ -99,15 +112,16 @@ Hard invariants that must hold throughout implementation: rather than delete it, preserving both behaviours. - Observation: The `_PATH_CHECK_TIMEOUT_SECONDS` constant is used in + validate_sandbox_diagnostics.py but logically belongs with path checks. Evidence: Used in `_execute_diagnostic_command()` for path diagnostic commands. Impact: Duplicated the constant in validate_sandbox_diagnostics.py to avoid circular import dependency while keeping path check timeout logic together. - ## Decision log - Decision: Rename second `_trim_output()` to `_trim_output_single_line()` instead + of consolidating into one function. Rationale: The two functions serve distinctly different purposes - multi-line diagnostic formatting vs single-line logging. Combining them would require @@ -116,6 +130,7 @@ Hard invariants that must hold throughout implementation: Date/Author: 2025-02-17 / Agent - Decision: Create modules in dependency order: formatters, diagnostics, path_checks, + arch, locators. Rationale: Avoids circular imports by ensuring each module only imports from modules created earlier or from existing validate_* modules. Formatters has no @@ -123,12 +138,14 @@ Hard invariants that must hold throughout implementation: Date/Author: 2025-02-17 / Agent - Decision: Move PolytheneSession imports to TYPE_CHECKING blocks in new modules. + Rationale: Ruff lint rule TC002 requires third-party imports used only for type hints to be in TYPE_CHECKING blocks. This reduces runtime import overhead and makes type-only dependencies explicit. Date/Author: 2025-02-17 / Agent - Decision: Keep validate_packages.py as orchestration layer only. + Rationale: Retained `_exec_with_diagnostics()`, `_install_and_verify()`, `_validate_package()`, `_MetadataValidators`, and public entry points `validate_deb_package()` and `validate_rpm_package()` in validate_packages.py. @@ -136,41 +153,48 @@ Hard invariants that must hold throughout implementation: orchestration layer. Date/Author: 2025-02-17 / Agent - ## Outcomes & retrospective Successfully refactored validate_packages.py from 734 lines to 377 lines by extracting 5 focused modules. All 635 tests pass, all commit gates pass. Modules created: + - validate_formatters.py (51 lines): Output trimming and stderr extraction - validate_arch.py (70 lines): Architecture mapping and platform detection -- validate_locators.py (49 lines): Package discovery (locate_deb, locate_rpm, ensure_subset) -- validate_sandbox_diagnostics.py (176 lines): Diagnostic collection for sandbox troubleshooting +- validate_locators.py (49 lines): Package discovery (locate_deb, locate_rpm, + ensure_subset) +- validate_sandbox_diagnostics.py (176 lines): Diagnostic collection for + sandbox troubleshooting - validate_path_checks.py (151 lines): Path validation with Python fallback logic What worked well: + - Dependency-ordered module creation prevented circular imports - Renaming `_trim_output()` variants instead of consolidating preserved clarity -- Re-exporting public functions via `__all__` in validate_packages.py maintained - backward compatibility - no test changes required +- Re-exporting public functions via `__all__` in validate_packages.py + maintained backward compatibility - no test changes required - TYPE_CHECKING blocks for type-only imports satisfied lint rules What could be improved: + - The `_PATH_CHECK_TIMEOUT_SECONDS` constant ended up duplicated between - validate_sandbox_diagnostics.py and validate_path_checks.py. A better approach - might be a shared constants module, but this would have added complexity for - marginal benefit. + validate_sandbox_diagnostics.py and validate_path_checks.py. A better + approach might be a shared constants module, but this would have added + complexity for marginal benefit. Lessons learned: + - When encountering duplicate functions, investigate usage patterns before - assuming consolidation is correct. Sometimes duplicates serve distinct purposes. + assuming consolidation is correct. Sometimes duplicates serve distinct + purposes. - Module extraction benefits from clear dependency analysis upfront. Creating a dependency graph before extraction would have made the ordering decision more obvious. + - Tests using dynamic module loading (via fixtures) work seamlessly with - refactored imports as long as re-exports are maintained. + refactored imports as long as re-exports are maintained. ## Context and orientation @@ -202,22 +226,29 @@ multi-line variant's signature (with line_limit and char_limit parameters). This must be resolved by consolidating into a single canonical implementation that preserves the intended behaviour for all call sites. - ## Plan of work Stage A: Audit and resolve the `_trim_output()` duplicate - Read lines 148-174 and 385-391 to understand both signatures and their + intended use cases. + - Search all call sites in validate_packages.py to determine which signature + each caller expects. + - Consolidate into a single canonical `_trim_output()` implementation that + preserves the multi-line variant's signature (line 148) with line_limit and char_limit parameters, since this provides the most flexibility. The single-line variant (line 385) can be expressed as a call to the multi-line variant with appropriate parameters. + - Update all call sites of the single-line variant to use the consolidated + implementation. + - Verify behaviour remains identical and tests pass before proceeding. Stage B: Create the five new modules @@ -244,8 +275,11 @@ Stage C: Update validate_packages.py - Add imports from the five new modules. - Update `__all__` to re-export public functions from new modules. - Ensure internal functions like `_exec_with_diagnostics`, + `_install_and_verify`, `_validate_package`, `_MetadataValidators` remain. + - Keep public entry points `validate_deb_package()` and + `validate_rpm_package()`. Stage D: Validation @@ -255,7 +289,6 @@ Stage D: Validation - Run `make lint` and fix any violations. - Run `make check-fmt` and fix formatting. - ## Concrete steps ### Stage A: Resolve _trim_output() duplicate @@ -284,6 +317,7 @@ Stage D: Validation Create `validate_formatters.py`: Functions to extract: + - _trim_output (line 148, multi-line variant) - _trim_output_single_line (line 385, renamed single-line variant) - _extract_process_stderr (line 166) @@ -291,6 +325,7 @@ Create `validate_formatters.py`: Create `validate_sandbox_diagnostics.py`: Functions to extract: + - _execute_diagnostic_command (line 177) - _build_path_diagnostic_commands (line 195) - _format_path_diagnostics (line 221) @@ -301,6 +336,7 @@ Create `validate_sandbox_diagnostics.py`: Create `validate_path_checks.py`: Constants and functions to extract: + - _PYTHON_FALLBACK_SCRIPT (line 27) - _PYTHON_FALLBACK_INTERPRETERS (line 30) - _PATH_CHECK_TIMEOUT_SECONDS (line 31) @@ -314,6 +350,7 @@ Create `validate_path_checks.py`: Create `validate_arch.py`: Functions to extract: + - _HOST_ARCH_ALIAS_MAP (line 57) - _host_architectures (line 72) - _should_skip_sandbox (line 81) @@ -323,6 +360,7 @@ Create `validate_arch.py`: Create `validate_locators.py`: Functions to extract: + - locate_deb (line 119) - locate_rpm (line 129) - ensure_subset (line 139) @@ -403,7 +441,6 @@ Expected output: "All checks passed!" Expected output: "173 files already formatted" - ## Validation and acceptance Quality criteria: @@ -420,7 +457,6 @@ Quality method: cd /home/user/project make check-fmt && make lint && make typecheck && make test - ## Idempotence and recovery All steps are idempotent: @@ -432,37 +468,46 @@ All steps are idempotent: Recovery: if a stage fails, revert the changes to that stage only and retry with corrections. Use git to track intermediate states. - ## Artifacts and notes (To be filled during implementation with key transcripts and observations) - ## Interfaces and dependencies New modules will have these public interfaces (exported via `__all__`): `validate_formatters.py`: + - `_trim_output(output: str, *, line_limit: int = 5, char_limit: int = 400) -> str` - `_extract_process_stderr(error: BaseException | None) -> str | None` `validate_arch.py`: + - `acceptable_rpm_architectures(arch: str) -> set[str]` - `rpm_expected_architecture(arch: str) -> str` `validate_locators.py`: -- `locate_deb(package_dir: Path, package_name: str, version: str, release: str) -> Path` -- `locate_rpm(package_dir: Path, package_name: str, version: str, release: str) -> Path` -- `ensure_subset(expected: Collection[str], actual: Collection[str], label: str) -> None` + +- `locate_deb(package_dir: Path, package_name: str, version: str, + release: str) -> Path` +- `locate_rpm(package_dir: Path, package_name: str, version: str, + release: str) -> Path` +- `ensure_subset(expected: Collection[str], actual: Collection[str], + label: str) -> None` `validate_sandbox_diagnostics.py`: + - All functions are internal (prefixed with `_`); no public exports needed. `validate_path_checks.py`: + - All functions are internal (prefixed with `_`); no public exports needed. Dependencies flow: + - validate_packages imports from all five new modules. - New modules import from existing modules (validate_exceptions, + validate_helpers, validate_metadata, validate_polythene). + - No circular dependencies. diff --git a/docs/rfc-ortho-config-env-aliases.md b/docs/rfc-ortho-config-env-aliases.md new file mode 100644 index 00000000..97f40a1b --- /dev/null +++ b/docs/rfc-ortho-config-env-aliases.md @@ -0,0 +1,752 @@ +# RFC: Environment Variable Aliases for ortho-config + +## Title + +Environment Variable Aliases for ortho-config: Support Dynamic Aliasing and +Precedence-Based Fallback + +## Status + +Draft + +## Date + +2026-06-18 + +## Summary + +This RFC proposes adding support for environment variable aliases in +ortho-config, allowing multiple names to resolve to the same configuration +parameter. The design supports both compile-time alias declaration (via +procedural derive macros) and runtime alias registration, with a clear +precedence model that respects canonical names over aliases. This feature +enables smoother migration for projects like vk and podbot that need to support +legacy variable names while standardizing on canonical ones. + +## Motivation + +### Use Case: vk + +The vk project maintains configuration across multiple deployment environments. +As vk evolves, environment variable naming conventions may change or +consolidate. For example: + +- Old convention: `VK_DATABASE_HOST`, `VK_DB_HOST` (inconsistent naming) +- New convention: `VK_DATABASE_HOST` (single canonical name) + +Without alias support, vk must either: + +1. Hardcode checks for multiple variable names throughout the codebase, +creating maintenance burden +2. Perform manual pre-processing of environment variables at startup +3. Maintain two parallel configuration systems indefinitely + +With aliases, vk can declare `VK_DB_HOST` as an alias for +`VK_DATABASE_HOST`, automatically falling back to the old name when the +canonical name is unset. This enables gradual migration without code +duplication. + +### Use Case: podbot + +Podbot integrates ortho-config for multi-tenant audio processing configuration. +As the platform matures: + +- Original var names used underscores: `PODBOT_AUDIO_SAMPLE_RATE_HZ` +- Standardization effort switches to kebab-case in configuration: `podbot.audio.sample-rate-hz` +- Internal library names differ from external configuration names + +Aliases allow podbot to: + +1. Define the production canonical name in configuration +2. Declare aliases for legacy environment variables, documentation names, and +integration points +3. Support multiple deployment scenarios (legacy CI/CD, containers, Kubernetes) +without code changes + +### Core Problem + +Both projects face this pattern: environment-first systems need to support +**backward compatibility** while **migrating to new naming schemes**. Current +solutions are ad-hoc and spread throughout application code. A standardized +mechanism in ortho-config would: + +- Centralize alias definitions at the configuration struct definition site +- Enable zero-cost aliases (checked at compile time where possible) +- Provide clear semantics for precedence and fallback behavior +- Reduce maintenance burden and human error + +## Proposed Solution + +### Derive Macro Syntax + +```rust +use ortho_config::Config; + +#[derive(Config)] +#[ortho_config(env_prefix = "VK_")] +struct DbConfig { + /// Primary database hostname + #[ortho_config( + env_name = "DATABASE_HOST", + aliases = ["DB_HOST", "DBHOST"] + )] + database_host: String, + + /// Connection pool size; legacy var name still accepted + #[ortho_config( + env_name = "POOL_SIZE", + aliases = ["CONNECTION_POOL_SIZE"], + default = "10" + )] + pool_size: u32, +} +``` + +Expansion semantics: + +- `DATABASE_HOST` (canonical) is checked first +- If unset, `DB_HOST` is checked +- If unset, `DBHOST` is checked +- If still unset, the default is used + +### Runtime API + +For dynamic alias registration: + +```rust +use ortho_config::{Config, Aliases}; + +// Register aliases at startup +let aliases = Aliases::new() + .alias("PODBOT_AUDIO_SR", "PODBOT_AUDIO_SAMPLE_RATE_HZ")? + .alias("LEGACY_MODE", "PODBOT_MODE")?; + +// When creating config, provide aliases +let config = PodBotConfig::from_env_with_aliases(&aliases)?; + +// Or use a builder pattern +let config = PodBotConfig::builder() + .with_aliases(aliases) + .from_env()?; +``` + +### Precedence Rules + +Aliases follow a strict precedence hierarchy: + +1. **Canonical environment variable** (highest priority) +2. **Aliases in declaration order** (left to right as listed in `aliases = [...]`) +3. **Default value** (if provided) +4. **Error** (if no default and no env vars set) + +Example walkthrough: + +```rust +#[ortho_config(env_name = "FOO", aliases = ["BAR", "BAZ"])] +field: String, +``` + +Resolution with environment state: + +| State | Resolved Value | Source | +| -------------------------- | -------------- | --------------------- | +| `FOO=a, BAR=b, BAZ=c` | `a` | Canonical (highest) | +| `BAR=b, BAZ=c` | `b` | First alias | +| `BAZ=c` | `c` | Second alias | +| `(none)` + `default="d"` | `d` | Default value | +| `(none)` + no default | Error | No source found | + +## Detailed Design + +### Macro Levels + +#### Compile-Time Macro Expansion + +The `#[ortho_config(...)]` attribute macro generates code that: + +1. **Statically generates the resolution chain** during compilation +2. **Unrolls the precedence checks** into individual environment lookups +3. **Emits constants** listing all possible names for documentation/debugging + +Example generated code: + +```rust +// Original: +#[ortho_config(env_name = "DATABASE_HOST", aliases = ["DB_HOST"])] +database_host: String, + +// Generates approximately: +const DATABASE_HOST_NAMES: &[&str] = &["DATABASE_HOST", "DB_HOST"]; + +// Within from_env() expansion: +let database_host = env::var("VK_DATABASE_HOST") + .or_else(|_| env::var("VK_DB_HOST")) + .context("DATABASE_HOST configuration")?; +``` + +#### Runtime Registration + +For scenarios where aliases cannot be known at compile time, a +`#[ortho_config(runtime_aliases)]` mode allows registration: + +```rust +#[derive(Config)] +#[ortho_config(env_prefix = "APP_", runtime_aliases = true)] +struct Config { + #[ortho_config(env_name = "MODE")] + mode: String, +} + +// At startup: +let mut aliases = Aliases::builder(); +if uses_legacy_system() { + aliases.alias("LEGACY_MODE", "APP_MODE")?; +} +let config = Config::from_env_with_aliases(aliases.build())?; +``` + +### Runtime API Details + +#### Alias Builder + +```rust +pub struct AliasesBuilder { /* ... */ } + +impl AliasesBuilder { + /// Register that `alias_name` is an alias for `canonical_name` + /// Both names are tested; canonical is tried first + pub fn alias( + mut self, + alias_name: &str, + canonical_name: &str, + ) -> Result { + // Validation: neither can be empty + // Validation: alias != canonical + // Validation: no cycles (A->B, B->A) + Ok(self) + } + + /// Build the immutable Aliases struct + pub fn build(self) -> Aliases { + Aliases { /* ... */ } + } +} + +pub struct Aliases { + // Internal: map of alias -> canonical + map: HashMap, +} + +impl Aliases { + pub fn resolve(&self, name: &str) -> &str { + // If name is an alias, return canonical name + // Otherwise, return name unchanged + self.map.get(name).map(|s| s.as_str()).unwrap_or(name) + } + + pub fn all_names(&self, canonical: &str) -> Vec<&str> { + // Return canonical and all aliases pointing to it + } +} +``` + +#### Integration with Config Trait + +```rust +pub trait Config: Sized { + /// Standard from_env() - uses only compile-time aliases + fn from_env() -> Result; + + /// With runtime aliases + fn from_env_with_aliases(aliases: &Aliases) -> Result; + + /// Merge aliases into resolution chain + fn resolve_env_var(name: &str, aliases: &Aliases) -> Result; +} +``` + +### Detailed Resolution Algorithm + +The resolution engine applies aliases at multiple levels: + +1. **Field-level resolution** (within a struct field) +2. **Nested struct resolution** (for composite configs) +3. **Array/list resolution** (for repeated config blocks) + +For each field: + +```rust +fn resolve_field(field_name, field_spec, aliases): + // field_spec = {env_name, aliases, default, ...} + + canonical = env_prefix + field_spec.env_name + candidates = [canonical] + [env_prefix + a for a in field_spec.aliases] + + for candidate in candidates: + if runtime_aliases provided: + candidate = runtime_aliases.resolve(candidate) + if env_var_exists(candidate): + return parse(env_var(candidate)) + + if field_spec.default: + return field_spec.default + else: + return Error +``` + +### Testing Strategy + +#### Unit Tests + +```rust +#[cfg(test)] +mod tests { + use ortho_config::*; + + #[test] + fn test_canonical_takes_precedence() { + // Set both canonical and alias + // Verify canonical is used + } + + #[test] + fn test_alias_used_when_canonical_absent() { + // Set only first alias + // Verify it's used + } + + #[test] + fn test_alias_order_respected() { + // Set second alias but not first + // Verify second alias is used + } + + #[test] + fn test_default_used_when_all_absent() { + // Don't set any env vars + // Verify default is used + } + + #[test] + fn test_error_when_required_absent() { + // Don't set any env vars, no default + // Verify error is returned with helpful message + } + + #[test] + fn test_cycle_detection() { + // Try to register A->B, B->A + // Verify error during build + } + + #[test] + fn test_nested_struct_aliases() { + // Config with nested struct containing aliases + // Verify aliases work transitively + } + + #[test] + fn test_runtime_override_of_compile_time_aliases() { + // Register runtime alias that shadows compile-time + // Verify correct precedence + } +} +``` + +#### Integration Tests + +```rust +#[cfg(test)] +mod integration_tests { + #[test] + fn test_vk_migration_path() { + // Simulate vk migration from VK_DB_HOST to VK_DATABASE_HOST + // Verify old vars still work in backward-compat mode + // Verify new vars take precedence + } + + #[test] + fn test_podbot_multi_naming_scenario() { + // Simulate podbot with legacy, current, and future naming + // Verify all resolve correctly with right precedence + } +} +``` + +#### Documentation Generation + +Aliases are included in generated documentation: + +```rust +#[derive(Config)] +struct MyConfig { + #[ortho_config(env_name = "FOO", aliases = ["BAR", "BAZ"])] + /// Main configuration + field: String, +} + +// Auto-generates: +// - docs/generated/config-reference.md listing all names per field +// - config help message showing acceptable names +// - migration guide for deprecated aliases +``` + +### Error Messages + +When configuration fails, errors clearly indicate all names tried: + +```text +Error: failed to load configuration field 'database_host' + +Tried in order: + + 1. VK_DATABASE_HOST (canonical) + 2. VK_DB_HOST (alias) + 3. VK_DBHOST (alias) + +No value found and no default set. + +Suggestion: Set one of the above environment variables. + +Learn more: +[https://docs.example.com/vk/config#database_host](https://docs.example.com/vk/config#database_host) +``` + +## Migration Path + +### Phase 1: vk Migration (Month 1) + +1. **Tag canonical variables** in vk's current code + - Define `VK_DATABASE_HOST` as the canonical name + - List all legacy names used historically + +2. **Declare aliases** in config structs + + ```rust + #[ortho_config(env_name = "DATABASE_HOST", aliases = ["DB_HOST", "DBHOST"])] + database_host: String, + ``` + +3. **Test backward compatibility** + - Run with old env var names only + - Verify old scripts still work + +4. **Document migration** + - Release notes: "Both `VK_DB_HOST` and new `VK_DATABASE_HOST` work" + - Deprecation timeline: "Legacy names supported through vX.Y" + +### Phase 2: podbot Migration (Month 2) + +1. **Inventory naming inconsistencies** + - Old: `PODBOT_AUDIO_SAMPLE_RATE_HZ` + - New canonical: `PODBOT_AUDIO_SAMPLE_RATE_HZ` (standardize) + - External/legacy: `PODBOT_AUDIO_SR`, `LEGACY_MODE`, etc. + +2. **Implement runtime alias registration** + + ```rust + let mut aliases = Aliases::builder(); + + // Legacy external integrations + aliases.alias("PODBOT_AUDIO_SR", "PODBOT_AUDIO_SAMPLE_RATE_HZ")?; + aliases.alias("LEGACY_MODE", "PODBOT_MODE")?; + + // Container/Kubernetes secrets using different scheme + if is_container_env() { + aliases.alias("AUDIO_RATE", "PODBOT_AUDIO_SAMPLE_RATE_HZ")?; + } + + let config = PodBotConfig::from_env_with_aliases(aliases.build())?; + ``` + +3. **Gradual deployment** + - Stage 1: Support both old and new in staging + - Stage 2: Require new names in production, warn on old ones + - Stage 3: Remove old names after grace period + +### Phase 3: Community Adoption + +vk and podbot publish: + +- Migration guides +- Before/after comparisons +- Case studies showing maintenance reduction + +Other projects adopt aliases for their own configurations. + +## Alternatives Considered + +### Alternative 1: Raw Environment Scanning at Startup + +**Approach**: Projects scan for multiple variable names and rename them before +initialization. + +```rust +// Anti-pattern: scattered throughout codebase +fn load_config() -> Result { + // Manual renaming + if let Ok(v) = env::var("VK_DB_HOST") { + env::set_var("VK_DATABASE_HOST", v); + } + if let Ok(v) = env::var("DBHOST") { + env::set_var("VK_DATABASE_HOST", v); + } + Config::from_env() +} +``` + +**Pros**: + +- No framework changes needed + +**Cons**: + +- Duplicated across projects +- Error-prone (easy to miss names) +- Hidden logic outside config struct definition +- Hard to document +- Can't optimize (multiple env lookups) +- Imperative, not declarative + +### Alternative 2: Separate Configuration Layer + +**Approach**: Add a "pre-processing" layer that normalizes environment before +passing to ortho-config. + +```rust +struct LegacyEnvMapper { + mappings: HashMap<&str, &str>, +} + +impl LegacyEnvMapper { + fn apply(&self) { + for (old, new) in &self.mappings { + if let Ok(v) = env::var(old) { + env::set_var(new, v); + } + } + } +} +``` + +**Pros**: + +- Loosely coupled to ortho-config +- Could be reused in other contexts + +**Cons**: + +- Requires explicit invocation before `from_env()` +- Easy to forget or misconfigure +- Still duplicates logic across projects +- Less efficient (double environment reads) +- Doesn't provide documentation benefits + +### Alternative 3: Environment Variable Configuration File + +**Approach**: Use a dotenv-like file listing aliases. + +```env +# aliases.env +ALIAS VK_DB_HOST -> VK_DATABASE_HOST +ALIAS VK_DBHOST -> VK_DATABASE_HOST +ALIAS PODBOT_AUDIO_SR -> PODBOT_AUDIO_SAMPLE_RATE_HZ +``` + +**Pros**: + +- Centralized +- Can be version-controlled + +**Cons**: + +- Another file to manage and synchronize +- Breaks single source of truth (separate from config struct) +- Requires parsing at runtime for every lookup +- Harder to discover (hidden in file, not visible in code) +- No type safety or compile-time validation + +### Why This RFC is Better + +The proposed approach: + +1. **Colocates** alias definitions with field definitions (single source of +truth) +2. **Provides** compile-time validation and optimization +3. **Supports** both compile-time and runtime aliases (flexible) +4. **Generates** documentation automatically +5. **Ensures** type safety through Rust's type system +6. **Reduces** boilerplate and human error + +## Drawbacks + +### Compilation Time Impact + +The macro expansion adds non-trivial code to each field with aliases: + +```rust +// Small for one alias, ~1 line of generated code per alias +// For 50-field config with ~2 aliases per field, adds ~100 lines +// Negligible impact on compile time, but worth benchmarking +``` + +**Mitigation**: + +- Profile macro expansion +- Offer `lazy = true` option if needed (defer to runtime resolution) + +### Documentation Burden + +Projects must maintain alias lists in two places: + +1. Source code (as attributes) +2. User documentation (manually) + +**Mitigation**: + +- Auto-generate `config-reference.md` from macro attributes +- Include generated docs in CI validation + +### Potential for Misuse + +Developers might declare excessive aliases, creating confusion: + +```rust +#[ortho_config( + env_name = "FOO", + aliases = ["BAR", "BAZ", "QUX", "QUUX", "CORGE", "GRAULT"] // Too many! +)] +field: String, +``` + +**Mitigation**: + +- Lint warning when >3 aliases per field +- Documentation recommends max 2-3 +- Migration guide emphasizes retiring old names + +### Runtime Alias Registration Complexity + +Runtime aliases add complexity to initialization: + +```rust +// More verbose than simple from_env() +let config = Config::from_env_with_aliases( + Aliases::builder() + .alias("A", "B")? + .alias("C", "D")? + .build() +)?; +``` + +**Mitigation**: + +- Provide builder method: `Config::with_runtime_aliases(|a| a.alias(...)?)` +- Document common patterns +- Examples for typical scenarios + +## Unresolved Questions + +### 1. Case Sensitivity + +Should aliases be **case-sensitive**? + +- **Current proposal**: Yes, case-sensitive (standard for Unix env vars) +- **Alternative**: Case-insensitive matching option + +**Open**: Should we offer a `case_insensitive = true` attribute? + +### 2. Deprecation Tracking + +How should deprecated aliases be tracked over time? + +```rust +#[ortho_config( + env_name = "NEW_NAME", + aliases = [ + ("OLD_NAME", "1.0", "Use NEW_NAME instead"), + ] +)] +``` + +**Open**: Should we add deprecation metadata and emit warnings? + +### 3. Environment Variable Precedence vs Aliases + +When both runtime and compile-time aliases exist for the same field, what wins? + +- **Current proposal**: Compile-time aliases tried first, then runtime aliases +- **Alternative**: Runtime aliases override compile-time + +**Open**: Should precedence be customizable or fixed? + +### 4. Array/List Handling + +For repeated config elements (e.g., multiple database replicas): + +```rust +#[ortho_config( + env_name = "REPLICA_URLS", + aliases = ["REPLICA_HOSTS", "DB_REPLICAS"] +)] +replicas: Vec, +``` + +**Open**: Do aliases apply to the entire list, or individually? + +### 5. Nested Struct Aliases + +When a nested struct has its own aliases, and the parent also does: + +```rust +#[derive(Config)] +struct Parent { + #[ortho_config(aliases = ["OLD_DB"])] + db: DbConfig, // DbConfig itself has aliases on fields +} +``` + +**Open**: Should nested aliases be merged, stacked, or isolated? + +### 6. Performance Profiling + +Before stabilizing, we need to measure: + +- Macro expansion overhead +- Runtime lookup performance vs. simple `env::var()` +- Memory overhead of Aliases struct + +**Open**: What are acceptable performance targets? + +## Next Steps + +1. **Implementation phase**: + - Write macro expansion code with comprehensive tests + - Build Aliases runtime API + - Integrate into ortho-config crate + +2. **Review phase**: + - Get feedback from vk and podbot teams + - Performance profiling against targets + - Test with real migration scenarios + +3. **Documentation phase**: + - Write user guide with examples + - Create migration playbooks for vk and podbot + - Generate reference documentation + +4. **Stabilization**: + - Gather feedback from early adopters + - Resolve unresolved questions + - Version bump and release + +## References + +- [12-Factor App Config](https://12factor.net/config) - environment-first + principle +- [Rust RFC 2407 Anonymous struct fields](https://rust-lang.github.io/rfcs/2407-anonymous-fields.html) + - related feature design +- [dotenv-rs](https://github.com/dotenv-rs/dotenv-rs) - prior art in Rust env + handling +- [Kubernetes environment variables](https://kubernetes.io/docs/tasks/configure-pod-container/define-environment-variable-container/) + - real-world naming patterns diff --git a/docs/scripting-standards.md b/docs/scripting-standards.md index 73c0167c..40b34351 100644 --- a/docs/scripting-standards.md +++ b/docs/scripting-standards.md @@ -108,7 +108,9 @@ def main( # Lists (whitespace/newline separated by default) formats: list[str] | None = None, - man_paths: Annotated[list[Path] | None, Parameter(env_var="INPUT_MAN_PATHS")] = None, + man_paths: Annotated[ + list[Path] | None, Parameter(env_var="INPUT_MAN_PATHS") + ] = None, deb_depends: list[str] | None = None, rpm_depends: list[str] | None = None, ): @@ -158,7 +160,9 @@ Guidance: - Per‑parameter environment names can be pinned for backwards compatibility: ```python - config_out: Annotated[Optional[Path], Parameter(env_var="INPUT_CONFIG_PATH")] = None + config_out: Annotated[ + Optional[Path], Parameter(env_var="INPUT_CONFIG_PATH") + ] = None ``` ## plumbum: command calling and pipelines @@ -256,7 +260,9 @@ f.write_text("1.2.3\n", encoding="utf-8") version = f.read_text(encoding="utf-8").strip() # Atomic write pattern (tmp → replace) -with tempfile.NamedTemporaryFile("w", delete=False, dir=f.parent, encoding="utf-8") as tmp: +with tempfile.NamedTemporaryFile( + "w", delete=False, dir=f.parent, encoding="utf-8" +) as tmp: tmp.write("new-contents\n") tmp_path = Path(tmp.name) diff --git a/docs/windows-gnullvm-build.md b/docs/windows-gnullvm-build.md index dcc070dc..0c63fe96 100644 --- a/docs/windows-gnullvm-build.md +++ b/docs/windows-gnullvm-build.md @@ -1,15 +1,33 @@ # Building for Windows `*-pc-windows-gnullvm` -This document describes adapting the `rust-build-release` action to build for Windows `gnullvm` targets. The configuration produces MinGW-style artefacts on a Windows runner using an MSVC host toolchain while linking against LLVM's MinGW CRT provided by `llvm-mingw`, thereby removing any dependency on `libgcc`. Both `x86_64-pc-windows-gnullvm` and `aarch64-pc-windows-gnullvm` are supported. +This document describes adapting the `rust-build-release` action to build for +Windows `gnullvm` targets. The configuration produces MinGW-style artefacts on a +Windows runner using an MSVC host toolchain while linking against LLVM's MinGW +CRT provided by `llvm-mingw`, thereby removing any dependency on `libgcc`. Both +`x86_64-pc-windows-gnullvm` and `aarch64-pc-windows-gnullvm` are supported. -The process is orchestrated through Python scripts within the action, which replace the PowerShell examples from the original recommendation. +The process is orchestrated through Python scripts within the action, which +replace the PowerShell examples from the original recommendation. ## Strategy Overview -1. **Host vs. Target**: The build runs on a `windows-latest` GitHub Actions runner, which uses the `stable-x86_64-pc-windows-msvc` toolchain as the _host_. This ensures any build scripts (`build.rs`) compile with MSVC and do not introduce GCC runtime dependencies. The _target_ is one of the `*-pc-windows-gnullvm` triples (currently `x86_64` or `aarch64`), which instruct `rustc` to produce a MinGW-style binary. -2. **`cross` Local Backend**: Instead of using Docker (which is unavailable on Windows runners for Linux containers), `cross` is instructed to use its "local backend" via the `CROSS_NO_DOCKER=1` environment variable. This uses the host's toolchains directly. -3. **LLVM Linker**: The key is to tell `rustc` how to link the `gnullvm` target. `clang` and `lld` from the `llvm-mingw` project are used, configured via a dynamically generated `.cargo/config.toml` file. -4. **Python Orchestration**: All setup steps—downloading `llvm-mingw`, creating the Cargo config, and setting environment variables—are handled by a dedicated Python script within the action, ensuring cross-platform consistency and maintainability. +1. **Host vs. Target**: The build runs on a `windows-latest` GitHub Actions + runner, which uses the `stable-x86_64-pc-windows-msvc` toolchain as the + _host_. This ensures any build scripts (`build.rs`) compile with MSVC and do + not introduce GCC runtime dependencies. The _target_ is one of the + `*-pc-windows-gnullvm` triples (currently `x86_64` or `aarch64`), which + instruct `rustc` to produce a MinGW-style binary. +2. **`cross` Local Backend**: Instead of using Docker (which is unavailable on + Windows runners for Linux containers), `cross` is instructed to use its + "local backend" via the `CROSS_NO_DOCKER=1` environment variable. This uses + the host's toolchains directly. +3. **LLVM Linker**: The key is to tell `rustc` how to link the `gnullvm` + target. `clang` and `lld` from the `llvm-mingw` project are used, + configured via a dynamically generated `.cargo/config.toml` file. +4. **Python Orchestration**: All setup steps—downloading `llvm-mingw`, creating + the Cargo config, and setting environment variables—are handled by a + dedicated Python script within the action, ensuring cross-platform + consistency and maintainability. ## Implementation within `rust-build-release` @@ -17,7 +35,9 @@ The following changes adapt the recommendation for the existing action. ### 1. GitHub Actions Workflow (`action.yml`) -A conditional step is added to `rust-build-release/action.yml` to trigger the setup whenever a Windows `gnullvm` target is requested. The target triple is passed through to the setup script. +A conditional step is added to `rust-build-release/action.yml` to trigger the +setup whenever a Windows `gnullvm` target is requested. The target triple is +passed through to the setup script. ```yaml # .github/actions/rust-build-release/action.yml @@ -28,28 +48,54 @@ A conditional step is added to `rust-build-release/action.yml` to trigger the se working-directory: ${{ inputs.project-dir }} run: | set -euo pipefail - uv run --script "$GITHUB_ACTION_PATH/src/setup_gnullvm.py" --target "${{ inputs.target }}" + uv run --script "$GITHUB_ACTION_PATH/src/setup_gnullvm.py" \ + --target "${{ inputs.target }}" - name: Build release # ... ``` -The composite action installs `uv` earlier via the "Setup uv" step, so the snippet -does not need to install it explicitly. +The composite action installs `uv` earlier via the "Setup uv" step, so the +snippet does not need to install it explicitly. ### 2. Setup Script (`setup_gnullvm.py`) -The Python helper automates the setup process previously handled by PowerShell. It performs the following actions: - -- **Downloads `llvm-mingw`**: Fetches the specified release archive from GitHub, extracts it to a temporary runner directory, and adds its `bin` directory to the `GITHUB_PATH`. On GitHub’s Windows runners (`x86_64` hosts), the setup defaults to the `ucrt-x86_64` archive, which bundles cross-compilers for both the `x86_64` and `aarch64` Windows targets. The `RBR_LLVM_MINGW_VARIANT` environment variable can override this default. When the requested variant is unavailable or lacks a registered checksum for the chosen release, the helper falls back to `ucrt-x86_64` so the job can proceed on x86_64 runners. -- **Creates `.cargo/config.toml`**: Generates the necessary configuration to instruct Cargo how to link the requested `gnullvm` target using the matching `*-w64-mingw32-clang` frontend. -- **Sets Environment Variables**: Writes the `CROSS_NO_DOCKER=1` flag and the target-scoped `CC_*`, `CXX_*`, `LD_*`, `AR_*`, and `RANLIB_*` variables to `GITHUB_ENV` for the subsequent build step to use. -- **Supports Overrides**: Optional `RBR_LLVM_MINGW_VERSION`, `RBR_LLVM_MINGW_VARIANT`, and `RBR_LLVM_MINGW_SHA256` environment variables allow selecting alternative releases while keeping checksum verification enabled. - -The default archive variant is `ucrt-x86_64`, matching the upstream `llvm-mingw` distribution that links against the UCRT and provides cross-compilers for multiple targets. The setup script supports variants that share the upstream archive layout, including `ucrt-i686`, `msvcrt-x86_64`, and `msvcrt-i686`. When overriding `RBR_LLVM_MINGW_VARIANT`, set `RBR_LLVM_MINGW_SHA256` to the checksum for the chosen archive to keep checksum validation enabled; otherwise the script automatically falls back to the default archive when a checksum is unavailable. +The Python helper automates the setup process previously handled by PowerShell. +It performs the following actions: + +- **Downloads `llvm-mingw`**: Fetches the specified release archive from + GitHub, extracts it to a temporary runner directory, and adds its `bin` + directory to the `GITHUB_PATH`. On GitHub's Windows runners (`x86_64` hosts), + the setup defaults to the `ucrt-x86_64` archive, which bundles + cross-compilers for both the `x86_64` and `aarch64` Windows targets. The + `RBR_LLVM_MINGW_VARIANT` environment variable can override this default. + When the requested variant is unavailable or lacks a registered checksum for + the chosen release, the helper falls back to `ucrt-x86_64` so the job can + proceed on x86_64 runners. +- **Creates `.cargo/config.toml`**: Generates the necessary configuration to + instruct Cargo how to link the requested `gnullvm` target using the matching + `*-w64-mingw32-clang` frontend. +- **Sets Environment Variables**: Writes the `CROSS_NO_DOCKER=1` flag and the + target-scoped `CC_*`, `CXX_*`, `LD_*`, `AR_*`, and `RANLIB_*` variables to + `GITHUB_ENV` for the subsequent build step to use. +- **Supports Overrides**: Optional `RBR_LLVM_MINGW_VERSION`, + `RBR_LLVM_MINGW_VARIANT`, and `RBR_LLVM_MINGW_SHA256` environment variables + allow selecting alternative releases while keeping checksum verification + enabled. + +The default archive variant is `ucrt-x86_64`, matching the upstream +`llvm-mingw` distribution that links against the UCRT and provides +cross-compilers for multiple targets. The setup script supports variants that +share the upstream archive layout, including `ucrt-i686`, `msvcrt-x86_64`, and +`msvcrt-i686`. When overriding `RBR_LLVM_MINGW_VARIANT`, set +`RBR_LLVM_MINGW_SHA256` to the checksum for the chosen archive to keep +checksum validation enabled; otherwise the script automatically falls back to +the default archive when a checksum is unavailable. ### 3. Build Script (`main.py`) -The main build script, `src/main.py`, is modified to recognise when the `cross` local backend should be used, even when no container runtime (Docker/Podman) is detected. +The main build script, `src/main.py`, is modified to recognise when the `cross` +local backend should be used, even when no container runtime (Docker/Podman) is +detected. ```python # src/main.py (excerpt) @@ -61,11 +107,14 @@ The main build script, `src/main.py`, is modified to recognise when the `cross` # ... ``` -This ensures that `cross` is invoked correctly when the setup script has prepared the environment for a local `gnullvm` build. +This ensures that `cross` is invoked correctly when the setup script has +prepared the environment for a local `gnullvm` build. ## How to Use -To build for a Windows `gnullvm` target, set the `target` input in a workflow that uses the `rust-build-release` action on a `windows-latest` runner. The action performs the setup automatically. +To build for a Windows `gnullvm` target, set the `target` input in a workflow +that uses the `rust-build-release` action on a `windows-latest` runner. The +action performs the setup automatically. ```yaml # .github/workflows/example-workflow.yml @@ -85,5 +134,5 @@ jobs: target: ${{ matrix.target }} # ... other inputs like project-dir, version, etc. -The matrix workflow above validates that both Windows `gnullvm` targets are configured correctly. -``` +The matrix workflow above validates that both Windows `gnullvm` targets are +configured correctly. diff --git a/rust-toy-app/README.md b/rust-toy-app/README.md index 8cde6ea4..7fb397c2 100644 --- a/rust-toy-app/README.md +++ b/rust-toy-app/README.md @@ -6,15 +6,15 @@ ## Purpose -This simple Command Line Interface (CLI) application demonstrates various Rust testing patterns for +This simple CLI application demonstrates various Rust testing patterns for coverage collection validation: | Test Type | Framework | File(s) | Harness | -|-----------|-----------|---------|---------| -| Unit tests | [rstest](https://crates.io/crates/rstest) | `src/lib.rs` | Standard | -| Integration tests | rstest + assert_cmd | `tests/cli.rs` | Standard | -| Behaviour-Driven Development (BDD) (Gherkin) | [cucumber-rs](https://cucumber-rs.github.io/cucumber/current/) | `tests/cucumber.rs`, `tests/features/*.feature` | Custom (`harness = false`) | -| BDD (rstest-style) | rstest | `tests/bdd.rs` | Standard | +| --- | --- | --- | --- | +| Unit | [rstest](https://crates.io/crates/rstest) | `src/lib.rs` | Standard | +| Integration | rstest + assert_cmd | `tests/cli.rs` | Standard | +| BDD (Gherkin) | [cucumber-rs](https://cucumber-rs.github.io/cucumber/current/) | `tests/features/*.feature` | Custom | +| BDD (rstest) | rstest | `tests/bdd.rs` | Standard | The test suite validates that the `generate-coverage` action correctly: @@ -97,7 +97,7 @@ The test suite is designed to be invoked via the `generate-coverage` action: ### Action Inputs Used | Input | Value | Description | -|-------|-------|-------------| +| --- | --- | --- | | `with-cucumber-rs` | `true` | Enables cucumber-rs coverage collection | | `cucumber-rs-features` | `tests/features` | Path to Gherkin feature files |