# xberg-io/actions
Shared GitHub Actions composite actions and reusable workflows for the xberg-io polyrepo.
## Actions
### Setup
| Action | Description |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setup-rust` | Rust toolchain with sccache, llvm-cov, cross-compilation targets |
| `setup-python-env` | Python and uv through mise, with dependency caching |
| `setup-node-workspace` | Node.js and pnpm through mise, with the workspace dependencies |
| `setup-openssl` | Cross-platform OpenSSL (Linux, macOS, Windows) |
| `setup-maven` | Maven through mise |
| `setup-go-cgo-env` | Go CGO environment for FFI builds |
| `setup-php` | PHP runtime setup |
| `setup-elixir` | Elixir / Erlang runtime setup |
| `setup-r` | R environment |
| `setup-chrome` | Chrome-compatible browser via `browser-actions/setup-chrome` (or aptmium on linux-arm64) |
| `setup-chromium` | Non-snap Chromium for headless e2e tests; uses Playwright browser installer; outputs `chromium-path` for direct binary use |
| [`setup-mise`](setup-mise/README.md) | mise and the tools in `mise.toml`, installed `--locked` against `mise.lock`, with caching |
| [`check-toolchain`](check-toolchain/README.md) | Fail when `mise.toml` drifts from the xberg-io/toolchains baseline named in `toolchains.toml` |
| `install-alef` | Alef CLI installation. `version` takes a semver tag, `latest` (reads the `alef.toml` pin), or `main`. A pinned version with no release archive and no buildable tag fails the job; set `allow-unreleased: true` to opt in to an unpinned `main` build instead |
| `install-wasi-sdk` | WASI SDK for WebAssembly |
| `install-homebrew-linux` | Linuxbrew (Homebrew for Linux) for runners that need to build bottles |
| `setup-textlint` | textlint + the xberg-io standard rule set in one batched pnpm add |
| `setup-onnx-runtime-gpu` | Download and stage the GPU/CUDA build of ONNX Runtime; exports `ORT_DYLIB_PATH` + `LD_LIBRARY_PATH` |
| `configure-maven-gpg` | Prefer `gpg2` (shimmed as `gpg`) and patch legacy `--pinentry-mode loopback` two-arg form in `pom.xml` |
### Build
`build-python-sdist` defaults to registry dependencies. Set `source-dependencies: workspace-closure`
to include internal Rust sources through maturin, using the same mode for release and dry-run artifacts.
This mode requires Python 3.11+ and Cargo and validates the internal dependency closure before packaging.
It preserves plain Maturin feature selection and lock requirements; conditional feature objects, nonempty
`tool.maturin.config` or `unstable-flags`, and local source overrides are unsupported and fail before packaging.
| Action | Description |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `build-rust-ffi` | Shared/static Rust FFI libraries with native static-link metadata and error diagnostics |
| `build-and-cache-binding` | Language binding build with intelligent caching |
| `build-python-wheels` | Python wheels via cibuildwheel/maturin |
| `build-python-sdist` | Python sdists with registry dependencies or an included Rust workspace source closure |
| `build-node-napi` | Node.js NAPI-RS native modules |
| `build-ruby-gem` | Platform-specific Ruby gems |
| `build-php-extension` | PHP extensions |
| `build-wasm-package` | WebAssembly packages |
| `build-rust-cli` | Rust CLI binaries |
| `build-go-ffi` | Build the FFI crate for one Rust target and bundle shared/static libs, native-link metadata, and the header for Go cgo |
| `build-java-natives` | Build the FFI crate for one Rust target, stage at Panama FFM `native/{classifier}/` layout |
| `build-csharp-natives` | Build the FFI crate for one Rust target, stage at NuGet `runtimes/{rid}/native/` layout |
| `build-elixir-natives` | Cross-compile a Rustler NIF for one Rust target, package as RustlerPrecompiled tar.gz |
| `build-dart-package` | flutter_rust_bridge codegen + cargo build for the Dart package's Rust crate |
| `build-swift-package` | cargo build + sync swift-bridge generated headers/sources into the Swift package |
| `build-zig-package` | cargo build the FFI crate + `zig build` smoke check |
| `homebrew-build-bottles` | Real Homebrew bottles via `brew install --build-bottle` + `brew bottle --json`, macOS ones tagged for the `bottle-floor` (default `sequoia`); uploads tarballs and saves bottle JSONs for the merge step |
| `homebrew-merge-bottles` | Aggregates per-platform bottle JSON manifests into formula `bottle do ... end` blocks (jq-driven, no brew needed on the runner); emits cellar values as Ruby symbols |
| `package-php-pie` | PIE-conventional binary archive from a built PHP extension |
| `rewrite-native-deps` | Rewrite a source package's workspace path-deps → registry version-deps via `alef publish prepare --require-registry` (embedded in `build-ruby-gem`/`build-php-extension`/`build-elixir-natives`/`build-python-sdist`) |
| `build-gpu-test-binary` | `cargo test --no-run` + JSON message parse to extract the executable for cross-runner GPU CI |
### Docs
| Action | Description |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `lint-docs` | Link check (lychee) + prose lint (textlint), with optional `alef snippets` validation of code examples. When `validate-snippets: "true"`, installs the `alef` CLI via `install-alef@v1` and runs the `docs:snippets:validate` task (or falls back to `alef snippets validate --snippets docs/snippets --level syntax` when neither runner defines it). Each docs task runs through mise when the consumer's mise config defines it, else the tool runs directly. Pin alef via the `alef-ref` input (defaults to `main`). |
| `build-docs` | Build the documentation site with [zensical](https://zensical.org). Sets up Python via `setup-python-env`, then runs the `docs:build:strict` task through mise (or `zensical build --strict --clean` as a fallback). `python-version` defaults to the Python the caller's `mise.toml` selects; `strict` defaults to `true`. `docs-group` selects the uv dependency group containing the docs builder (default `doc`; pass `docs` for repos that use the plural name). |
### Publish
All publish actions are idempotent: re-running on an already-published version
exits 0 (output `skipped=true` where applicable). Most accept `dry-run: 'true'`
to print the command without executing.
| Action | Target | Auth | Description |
| -------------------------------------------- | -------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `publish-pypi` | PyPI | OIDC trusted publisher | Python packages via uv, with optional verified storage quota fallback |
| [`archive-python`](archive-python/README.md) | GitHub Releases + Pages | GitHub token | Permanent distributions and a hash-pinned PEP 503 index |
| `publish-npm` | npm | `NPM_TOKEN` | `.tgz` files from a directory or direct `package-dir`; supports `npm-tag` (`latest`/`next`/`rc`) |
| `publish-crates` | crates.io | OIDC (`rust-lang/crates-io-auth-action`) | Rust crates with dependency-ordered publishing |
| `publish-rubygems` | RubyGems.org | `GEM_HOST_API_KEY` or trusted publisher | Ruby gems |
| `publish-maven` | Maven Central | `MAVEN_USERNAME` + `MAVEN_PASSWORD` + GPG | Java / `pom.xml` projects via `mvn deploy` |
| `publish-maven-gradle` | Maven Central | same as `publish-maven` | Kotlin / Java-Gradle via `gradle publishAndReleaseToMavenCentral` (or any task); imports GPG into the agent |
| `publish-nuget` | NuGet.org | OIDC trusted publisher (preferred) or `NUGET_API_KEY` | .NET `.nupkg` files |
| `publish-packagist` | Packagist | `PACKAGIST_API_TOKEN` | Triggers re-index for PHP packages auto-discovered from Git tags |
| `publish-hex` | Hex.pm | `HEX_API_KEY` | Elixir packages via `mix hex.publish` |
| `publish-gleam` | Hex.pm | `HEX_API_KEY` | Gleam packages via `gleam publish`; reuses the same `HEX_API_KEY` as `publish-hex` |
| `publish-pub` | pub.dev | OIDC trusted publisher | Dart packages; requires a one-time pub.dev claim of the package |
| `publish-zig` | Git tag | none | Validates `build.zig.zon` + tag; emits the tarball SHA-256 downstream consumers need for `build.zig.zon`'s `hash` field; can append a fetch snippet to the GH release notes. Optional `multi-platform-ffi-dir` bundles per-RID FFI libs + rewrites `build.zig` so a single tarball serves all supported targets |
| `publish-homebrew-source-formulas` | Homebrew tap | `HOMEBREW_TOKEN` | Render source formulas from release assets |
| `publish-scoop-manifests` | Scoop bucket | bot app token with write access to the bucket | Render Windows app manifests from the `*-pc-windows-msvc.zip` release assets |
| `publish-github-release` | GitHub Releases | `GITHUB_TOKEN` | Release-asset uploads |
| `publish-helm-chart` | OCI registry (GHCR, GAR, ECR, …) | username + password / token | Stamps `version`/`appVersion`, runs `helm dependency build` + `package` + `push`; idempotent on re-publish |
**Idempotency contract.** Each `publish-*` action either:
1. Detects "already published" output from the underlying tool (case-insensitive grep) and exits 0 with `skipped=true`, **or**
2. Has no registry side effect to be idempotent about — `publish-zig` is Git-tag-only and re-running it is naturally a no-op.
Pair with `check-registry` to skip the publish step entirely when the version
is already live (pre-flight gate; faster than relying on publish-time
idempotency, since the build/auth steps are skipped too). `check-registry`
covers `pypi`, `npm`, `wasm`, `rubygems`, `hex`, `maven`, `nuget`, `cratesio`,
`packagist`, `homebrew`, and `github-release`.
To gate on several packages at once, pass them via `extra-packages` and read the
combined verdict from `all-exist`, or a single package's from
`fromJSON(steps.<id>.outputs.results).<key>`. The per-line keys themselves are
not step outputs: a composite action propagates only the outputs it declares, so
`steps.<id>.outputs.<key>` is always the empty string.
### Release Infrastructure
| Action | Description |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `prepare-release-metadata` | Extract tag/version/ref/targets from workflow events (alef-backed; emits 19 `release_*` flags incl. dart/swift/gleam/zig/kotlin) |
| `validate-versions` | Cross-manifest version consistency checks |
| `retag-for-republish` | Delete and recreate Git tags for republishing |
| `generate-elixir-checksums` | RustlerPrecompiled NIF checksum generation |
| `check-registry` | Check if a package version exists on any registry |
| `wait-for-package` | Poll registries until a version becomes available |
| `verify-platform-packages` | Assert every platform package a napi-style parent declares in `optionalDependencies` exists — as a built `*.node` before publishing (`mode: binaries`) and as a resolvable registry version after (`mode: registry`). Reads the declared set from `optionalDependencies` and the expected cardinality from `napi.targets`, prints the examined counts, and fails on an empty set, an unmatched glob, or any set/count mismatch |
| `upload-release-assets` | Generic GH Release uploader; glob-expand a list of paths/patterns and upload with `--clobber` |
| `verify-release-assets` | Verify the GH Release contains every expected asset (fnmatch patterns, optional min size) |
| `prepare-swift-release` | Pre-tag half of an immutable Swift release: resolve the tag, derive Apple deployment targets from `Package.swift`, pin the bundle checksum with alef, verify the pin is the only change, then push the pin commit and the tag atomically without force; see [its README](prepare-swift-release/README.md) |
| `publish-swift-release` | Verify an immutable release tag's Swift manifest against the exact prepared bundle, upload it if absent and create the legacy `release/swift/<version>` branch create-only; never moves a tag |
| `finalize-release` | Edit GH Release from draft → published, set/clear prerelease, optionally create Go module tag |
| `retract-incomplete-release` | Return an incomplete release to draft — but only while no Homebrew tap or Scoop bucket already advertises its assets, since re-drafting there 404s every install that resolves through them |
| `render-runtime-json` | Render a NuGet `runtime.json` from a `runtime.json.template`, substituting the release version for the `{{VERSION}}` token before `dotnet pack` |
| `announce-release-discord` | Post a release announcement to Discord (skips RC tags, dedup via release-asset marker) |
### Test
| Action | Description |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `run-test-apps` | Generate and run e2e fixture suite via `alef test-apps` for a published binding version. Used post-publish to validate bindings against their registry-installed versions. Handles alef-cli install, language toolchain setup, version pin override in `alef.toml`, and captures logs. |
| `test-java-ffi` | Java Panama FFI test setup |
| `run-api-contract-tests` | Run schemathesis property-based contract tests against a containerised API |
### Utility
| Action | Description |
| ------------------------- | ---------------------------------------------------------------------------------- |
| `free-disk-space-linux` | Free disk space on Linux runners |
| `cache-binding-artifact` | Generic artifact caching for compiled bindings |
| `cleanup-rust-cache` | Clean Rust build artifacts |
| `restore-cargo-cache` | Restore Cargo cache |
| `check-docker-image-size` | Inspect a locally-loaded image, warn or fail on size threshold, write step summary |
| `pack-source-bundle` | Tar+zstd a set of paths into a release-ready bundle with sha256 sidecar |
## Reusable Workflows
| Workflow | Description |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reusable-validate.yml` | Poly formatting/lint validation with optional fixture-backed Alef snippet generation, tracked-drift detection, and strict uncached snippet checks (`check-fixture-snippets: true`; `alef-version` defaults to `latest`) |
| `reusable-validate-pr.yml` | PR title conventional commit validation |
| `reusable-validate-issues.yml` | Issue title validation |
| `reusable-check-registries.yml` | Matrix registry checks (replaces N separate check jobs) |
| `reusable-python-publish.yml` | Python package build and PyPI publish |
| `reusable-python-lint.yml` | Python linting via uv + prek |
| `reusable-docs-deploy.yml` | Build an Astro Starlight docs site and deploy it to GitHub Pages |
| `reusable-ai-rulez-validation.yml` | AI-Rulez validation (setup-ai-rulez, validate-ai-rulez, optional plugin verification) and the agent-conventions context-exposure budget gate |
| `reusable-toolchain.yml` | Check the toolchain against the xberg-io/toolchains baseline (`check-toolchain`) and install from the lock (`setup-mise`) |
| `reusable-actions-bump.yml` | Move every `uses:` ref to its latest major version with gh-actions-updater, run actionlint, then push to the default branch or open a pull request (`mode: push` or `pr`) |
| `reusable-swift-release-prepare.yml` | Build the Swift artifact bundle once on macOS, pin its checksum into `Package.swift`, push that commit and the release tag atomically, and attach the exact ZIP and receipt to a draft release; Publish Release then verifies and reuses it ([two-step procedure](prepare-swift-release/README.md#two-step-swift-release)) |
| `reusable-cli-release.yml` | Build Rust CLI binaries across a target matrix and upload them to a GitHub release; `macos-deployment-target` (e.g. `26.0`) sets `MACOSX_DEPLOYMENT_TARGET` on the macOS legs, which a caller's workflow-level `env` cannot reach |
`reusable-validate.yml` takes `check-toolchain: true` to run [`check-toolchain`](check-toolchain/README.md) before
any setup step, failing on drift from the xberg-io/toolchains baseline named in `toolchains.toml`.
`reusable-validate.yml` takes `lint-action-pins: true` to fail on any remote `uses:` ref that is not a bare major
tag, through `gau --lint-pins` (gh-actions-updater 0.4.0 or newer in the caller's `mise.toml`). An upstream with no
major tag goes in `allow_full` in `.gh-actions-updater.toml`.
`reusable-actions-bump.yml` needs a caller that grants `contents: write` and `pull-requests: write` and owns the
schedule. GitHub rejects a push from `GITHUB_TOKEN` that changes a file under `.github/workflows`, so pass the
`app-id` and `app-private-key` secrets of a GitHub App with contents, pull-requests and workflows write on the
caller: the job mints an installation token scoped to the caller repository and checks out, pushes and opens the
pull request with it. A pull request opened with the app token triggers the caller's CI; one opened with
`GITHUB_TOKEN` triggers no workflow. Without the secrets the job uses `GITHUB_TOKEN` and fails before pushing when
the bump changes a workflow.
```yaml
on:
schedule:
- cron: "17 6 * * 1"
workflow_dispatch:
concurrency:
group: actions-bump
cancel-in-progress: false
jobs:
bump:
permissions:
contents: write
pull-requests: write
uses: xberg-io/actions/.github/workflows/reusable-actions-bump.yml@v1
with:
mode: pr
secrets:
app-id: ${{ secrets.BOT_APP_ID }}
app-private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}
```
`reusable-ai-rulez-validation.yml` and `reusable-toolchain.yml` hold only the job. The caller keeps its triggers and
owns concurrency: neither sets any, because a job-level `concurrency` in a called workflow overrides the caller's.
Their steps use `xberg-io/actions/*@v1`, so they need a `v1` that already contains those composites.
```yaml
jobs:
validate:
uses: xberg-io/actions/.github/workflows/reusable-ai-rulez-validation.yml@v1
with:
expected-config-count: "2"
additional-config-directories: plugin/.ai-rulez
verify-plugin: true
budgets: |
claude=5850
codex=6450
```
## Toolchains through mise
Every composite and reusable workflow here that needs a language or tool installs it through mise with
[`setup-mise`](setup-mise/README.md), so its version comes from the caller's `mise.toml` and `mise.lock`. A version
input (`python-version`, `node-version`, `java-version`, `zig-version`, ...) still selects a version: it is matched
against the caller's locks (`3.12` selects a locked `3.12.x`) and installs unlocked, with a notice, when no lock
covers it. Empty, the default, takes the version the caller's `mise.toml` declares and fails when it declares none.
PHP, Swift, R, Android, Dart/Flutter, BEAM, .NET and Rust (`rust-toolchain.toml`) stay on their setup actions, as do
Ruby on Windows and the actions' own helpers (uv for clang-format, AI-Rulez's installer).
For repositories with compiled binding workspaces, set `prose-dependency-directory: docs-site` on the docs workflow and declare textlint and its rules in that standalone package. The default remains `.`; configuration, ignore files, and document paths stay relative to the repository root. Both setups use frozen installation and fail when prose lint fails.
## Repository Workflows
| Workflow | Description |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `test-unit.yml` | Unit test suite for helper scripts |
| `test-bats.yml` | Linux and macOS Bats suite for Bash action scripts |
| `test-integration.yml` | Integration tests for selected composite actions |
| `test-setup-mise.yml` | `setup-mise` on Linux x64/arm64, macOS and Windows, a stale-lock failure, and `check-toolchain` pass/drift |
| `test-free-disk-space.yml` | Smoke test for disk cleanup |
| `test-publish-actions.yml` | Publish action test workflow |
| `test-publish-scoop-manifests.yml` | Dry-run render test for `publish-scoop-manifests` |
| `test-setup-maven.yml` | Smoke test for Maven setup |
| `test-setup-node-workspace.yml` | Smoke test for Node workspace setup |
| `test-setup-openssl.yml` | Smoke test for OpenSSL setup |
| `test-setup-python-env.yml` | Smoke test for Python setup |
| `test-setup-rust.yml` | Smoke test for Rust setup |
| `test-validate.yml` | Validation workflow tests |
| `validate-pr.yml` | Repository PR validation |
| `validate-issues.yml` | Repository issue validation |
| `actions-bump.yml` | Weekly `reusable-actions-bump.yml` run in `pr` mode |
## Usage
### Composite actions
```yaml
- uses: xberg-io/actions/setup-rust@v1
with:
use-sccache: "true"
- uses: xberg-io/actions/setup-mise@v1
- uses: xberg-io/actions/publish-npm@v1
with:
packages-dir: dist
dry-run: "false"
- uses: xberg-io/actions/run-test-apps@v1
with:
language: python
version: 0.3.0
alef-version: latest
```
Set `cargo-zigbuild-revision` on `setup-rust` to a full lowercase 40-character commit from the official
cargo-zigbuild repository when a cross-build needs an unreleased upstream fix. The action verifies
Cargo's recorded source commit before making the installed tool available.
Matrix usage for post-publish e2e validation:
```yaml
jobs:
test-bindings:
strategy:
matrix:
language: [python, node, ruby, php, go, java, csharp, dart, swift]
fail-fast: false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: xberg-io/actions/run-test-apps@v1
with:
language: ${{ matrix.language }}
version: ${{ github.ref_name }}
alef-version: latest
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-logs-${{ matrix.language }}
path: /tmp/test-apps-*.log
```
### Reusable workflows
```yaml
jobs:
validate-pr:
uses: xberg-io/actions/.github/workflows/reusable-validate-pr.yml@main
publish:
uses: xberg-io/actions/.github/workflows/reusable-python-publish.yml@main
with:
package-name: my-package
```
## AI governance and plugin releases
| Action | Purpose |
| --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| [`setup-ai-rulez`](setup-ai-rulez/action.yml) | Install the latest ai-rulez release, or build a full immutable source revision |
| [`validate-ai-rulez`](validate-ai-rulez/action.yml) | Lint authored governance, verify locks and public skills, and reject tracked output drift |
| [`verify-ai-rulez-plugin`](verify-ai-rulez-plugin/action.yml) | Check product version synchronization, strict source validation, generated bundle freshness and provenance |
| [`package-ai-rulez-plugin`](package-ai-rulez-plugin/action.yml) | Package verified release archives, checksums and pinned marketplace indexes |
Install the reviewed CLI once with `setup-ai-rulez`, then run the verification and packaging
steps with that same executable. Use `config-directory: .ai-rulez` for the repository's
marketplace configuration; it composes `config.plugins.toml` and product member configurations.
Product sources remain under `plugin/.ai-rulez/`. Set `working-directory` for monorepo callers.
Packaging requires a release tag and a clean committed release tree; its outputs can be uploaded
as release assets without regenerating the checkout.
Set `cursor-marketplace: "true"` when every product member includes the Cursor runtime.
This adds a verified `emit/cursor-team-marketplace/` tree, containing its marketplace
index and plugin resources, alongside the native archives and Agent Plugins export.
The plugin test workflow exercises packaging and invalid-source failures. Repository governance
validation also lints authored skills, commands and configuration. Local Poly hooks validate
staged sources and shipped plugin artifacts; `mise run governance:check` checks ignored runtime
files in the live checkout.
## Development
```bash
# Install dependencies
mise run setup
# Run every suite: pytest, Bats, and Pester
mise run test
# Run Bash action tests (requires Bats)
mise run test:bats
# Run PowerShell action tests (requires PowerShell 7 and Pester)
mise run test:pester
# Lint
mise run lint
```
Python helpers are tested with pytest; Bash scripts are tested with Bats; PowerShell scripts are tested
with Pester. Install Bats locally through your platform package manager, or use the repository's
`install-bats` action in GitHub Actions. The action installs a pinned Bats version by default and logs
the resolved version. `latest` is still accepted, but it must be typed: a floating default made every
run's reproducibility depend on nobody omitting the input.
`install-bats` verifies the download against a digest pinned in `install-bats/checksums.tsv`. The
digest covers the decompressed tar rather than the `.tar.gz` as served, because GitHub generates
source archives on demand and does not guarantee the compressed bytes are stable. A version absent
from the table still installs but logs a warning saying it was not verified.
`install-pester` is stricter. Its `version` input must name a release listed in
`install-pester/checksums.tsv`, and the downloaded package is verified against that pinned SHA256
before anything is extracted; `latest` is rejected outright, because a floating version cannot be
checksum-verified.
### Shell test policy
Use Bats for Bash scripts on Linux and macOS. Consumer repositories should install it with
`xberg-io/actions/install-bats` and execute suites with `xberg-io/actions/run-bats`, both pinned to
an immutable actions revision. PowerShell scripts are not Bats-compatible; their Windows coverage
belongs in Pester suites, installed with `xberg-io/actions/install-pester` and executed with
`xberg-io/actions/run-pester` -- the same setup/run split, with typed inputs in place of run-bats'
argv list because `Invoke-Pester` takes a configuration object rather than command-line words.