Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/actions/install-nixie/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Changelog

## v1.0.0 (Unreleased)

- Add pinned Nixie and Merman CLI installation.
- Prefer `cargo binstall` for Merman and fall back to a locked source build.
- Support version overrides for Nixie, Merman CLI, and Python.
62 changes: 62 additions & 0 deletions .github/actions/install-nixie/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Install Nixie

Install pinned Nixie and Merman CLI releases for Mermaid validation.

The action installs Nixie through `uv` and its Merman rendering backend through
Cargo. It uses `cargo binstall` when available and falls back to a locked
`cargo install` build from crates.io.

## Inputs

| Name | Description | Required | Default |
| ---------------- | ---------------------------------------- | -------- | ------- |
| `nixie-version` | Nixie CLI version to install | no | `1.1.0` |
| `merman-version` | Merman CLI version to install | no | `0.7.0` |
| `python-version` | Python version used to install Nixie | no | `3.14` |

## Outputs

| Name | Description |
| ------ | ------------------------------------------------------- |
| _None_ | The action emits no outputs. |

## Usage

```yaml
- name: Set up Rust
uses: leynos/shared-actions/.github/actions/setup-rust@v1

- name: Install Nixie
uses: leynos/shared-actions/.github/actions/install-nixie@v1

- name: Validate Mermaid diagrams
run: nixie --renderer merman
```

To override the pinned versions:

```yaml
- uses: leynos/shared-actions/.github/actions/install-nixie@v1
with:
nixie-version: "1.1.0"
merman-version: "0.7.0"
python-version: "3.14"
```
Comment on lines +25 to +44

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the required repository-local usage example.

Add uses: ./.github/actions/install-nixie@v1 alongside the published-action example.

Triage: [type:docstyle]

As per coding guidelines, “Each action README must include … a usage example using uses: ./.github/actions/<name>@<major>.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/actions/install-nixie/README.md around lines 25 - 44, Add a
repository-local usage example to the install-nixie README alongside the
published-action example, using the required uses:
./.github/actions/install-nixie@v1 syntax. Keep the existing published-action
and version-override examples unchanged.

Sources: Coding guidelines, Path instructions


## Behaviour

- **Prerequisites**: `cargo` and `uv` must already be available on `PATH`.
The repository's `setup-rust` action provisions both tools and
`cargo-binstall`.
- **Merman installation**: When `cargo binstall` is available, the action
installs the selected Merman release from a binary package with locked
metadata. Otherwise it builds the exact selected release from crates.io with
`cargo install --locked`.
- **Nixie installation**: The action uses `uv tool install` with the selected
Python and exact Nixie release.
- **Failure behaviour**: Missing prerequisites and failed installations stop
the action immediately with a non-zero exit status.

## Release history

See [CHANGELOG](CHANGELOG.md).
44 changes: 44 additions & 0 deletions .github/actions/install-nixie/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Install Nixie
description: Install pinned Nixie and Merman CLI releases for Mermaid validation
inputs:
nixie-version:
description: Nixie CLI version to install
required: false
default: "1.1.0"
merman-version:
description: Merman CLI version to install
required: false
default: "0.7.0"
python-version:
description: Python version used by uv to install Nixie
required: false
default: "3.14"
runs:
using: composite
steps:
- name: Install Nixie and Merman CLI
shell: bash
env:
NIXIE_VERSION: ${{ inputs.nixie-version }}
MERMAN_VERSION: ${{ inputs.merman-version }}
PYTHON_VERSION: ${{ inputs.python-version }}
run: |
set -euo pipefail

if ! command -v cargo >/dev/null 2>&1; then
echo "::error::cargo is required to install merman-cli" >&2
exit 1
fi
if ! command -v uv >/dev/null 2>&1; then
echo "::error::uv is required to install nixie-cli" >&2
exit 1
fi

if cargo binstall --version >/dev/null 2>&1; then
cargo binstall --no-confirm --locked "merman-cli@${MERMAN_VERSION}"
else
echo "cargo-binstall unavailable; building merman-cli from crates.io"
cargo install --locked merman-cli --version "=${MERMAN_VERSION}"
fi

uv tool install --python "${PYTHON_VERSION}" "nixie-cli==${NIXIE_VERSION}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Export the Nixie executable directory to subsequent workflow steps.

Append $(uv tool dir --bin) to $GITHUB_PATH after installation, then extend the behavioural test to assert the exported entry. uv tool install uses a configurable tool-bin directory, while $GITHUB_PATH is the GitHub Actions mechanism that exposes directories to later steps; without this, the documented nixie --renderer merman command fails on runners where that directory is absent from PATH. (docs.astral.sh)

         uv tool install --python "${PYTHON_VERSION}" "nixie-cli==${NIXIE_VERSION}"
+        printf '%s\n' "$(uv tool dir --bin)" >> "$GITHUB_PATH"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/actions/install-nixie/action.yml at line 44, After the uv tool
installation command in the action’s install step, append the directory returned
by uv tool dir --bin to GITHUB_PATH so subsequent workflow steps can locate
nixie; update the associated behavioral test to assert that this directory is
exported.

Source: Linked repositories

152 changes: 152 additions & 0 deletions .github/actions/install-nixie/tests/test_action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""Behavioural tests for the install-nixie composite action."""

from __future__ import annotations

import os
import shutil
import subprocess
import typing as typ
from pathlib import Path

import pytest
import yaml

ACTION_PATH = Path(__file__).resolve().parents[1] / "action.yml"


def _load_action() -> dict[str, typ.Any]:
"""Load the install-nixie action manifest."""
return yaml.safe_load(ACTION_PATH.read_text(encoding="utf-8"))


def _install_script() -> str:
"""Return the action's installation shell fragment."""
steps = _load_action()["runs"]["steps"]
assert len(steps) == 1, "install-nixie should have one atomic install step"
run_script = steps[0].get("run")
assert isinstance(run_script, str), "install step must define a shell script"
return run_script


def _write_executable(path: Path, content: str) -> None:
"""Write an executable command stub."""
path.write_text(content, encoding="utf-8")
path.chmod(0o755)


def _run_install_script(
tmp_path: Path,
*,
binstall_available: bool,
include_cargo: bool = True,
include_uv: bool = True,
) -> subprocess.CompletedProcess[str]:
"""Execute the install fragment against deterministic command stubs."""
bash = shutil.which("bash")
if bash is None:
pytest.skip("bash not found on PATH")

stubs_dir = tmp_path / "stubs"
stubs_dir.mkdir()
calls_path = tmp_path / "calls"
if include_cargo:
binstall_status = 0 if binstall_available else 1
_write_executable(
stubs_dir / "cargo",
f"""#!/bin/bash
set -euo pipefail
if [ "${{1:-}}" = "binstall" ] && [ "${{2:-}}" = "--version" ]; then
exit {binstall_status}
fi
printf 'cargo' >> "$CALLS_PATH"
printf ' <%s>' "$@" >> "$CALLS_PATH"
printf '\n' >> "$CALLS_PATH"
""",
)
if include_uv:
_write_executable(
stubs_dir / "uv",
"""#!/bin/bash
set -euo pipefail
printf 'uv' >> "$CALLS_PATH"
printf ' <%s>' "$@" >> "$CALLS_PATH"
printf '\n' >> "$CALLS_PATH"
""",
)

env = {
**os.environ,
"CALLS_PATH": calls_path.as_posix(),
"MERMAN_VERSION": "0.7.0",
"NIXIE_VERSION": "1.1.0",
"PATH": stubs_dir.as_posix(),
"PYTHON_VERSION": "3.14",
}
Comment on lines +77 to +84

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test overridden input pins end-to-end.

Parameterize the injected Nixie, Merman, and Python versions, then assert that non-default values reach both installer commands. Current tests only exercise defaults, so configurable input propagation is unprotected.

As per coding guidelines, “Contract tests must validate that declared action inputs and outputs round-trip correctly.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/actions/install-nixie/tests/test_action.py around lines 77 - 84,
Update the test setup around the environment passed to the action to
parameterize injected Nixie, Merman, and Python versions with non-default
values. Assert end-to-end that each parameterized value is propagated to both
installer commands, while retaining coverage for the defaults and validating the
declared action input/output round trip.

Source: Coding guidelines

return subprocess.run( # noqa: S603,TID251 - exercise the bash fragment.
[bash, "-c", _install_script()],
cwd=tmp_path,
env=env,
capture_output=True,
text=True,
timeout=30,
)
Comment on lines +85 to +92

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Set check=False explicitly.

Preserve the intentional non-zero return-code assertions while satisfying Ruff’s PLW1510 finding.

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 85-85: subprocess.run without explicit check argument

Add explicit check=False

(PLW1510)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/actions/install-nixie/tests/test_action.py around lines 85 - 92,
Update the subprocess.run invocation in the test helper to set check=False
explicitly. Preserve the existing non-zero return-code assertions and all other
subprocess options unchanged.

Source: Linters/SAST tools



def test_manifest_exposes_pinned_version_inputs() -> None:
"""The action should expose the reviewed Nixie toolchain pins."""
manifest = _load_action()

assert manifest["runs"]["using"] == "composite"
assert manifest["inputs"]["nixie-version"]["default"] == "1.1.0"
assert manifest["inputs"]["merman-version"]["default"] == "0.7.0"
assert manifest["inputs"]["python-version"]["default"] == "3.14"


def test_install_script_prefers_cargo_binstall(tmp_path: Path) -> None:
"""Merman should use a locked binary install when cargo-binstall exists."""
result = _run_install_script(tmp_path, binstall_available=True)

assert result.returncode == 0, result.stderr
assert (tmp_path / "calls").read_text(encoding="utf-8").splitlines() == [
"cargo <binstall> <--no-confirm> <--locked> <merman-cli@0.7.0>",
"uv <tool> <install> <--python> <3.14> <nixie-cli==1.1.0>",
]


def test_install_script_falls_back_to_cargo_install(tmp_path: Path) -> None:
"""Merman should use a locked source build without cargo-binstall."""
result = _run_install_script(tmp_path, binstall_available=False)

assert result.returncode == 0, result.stderr
assert "cargo-binstall unavailable" in result.stdout
assert (tmp_path / "calls").read_text(encoding="utf-8").splitlines() == [
"cargo <install> <--locked> <merman-cli> <--version> <=0.7.0>",
"uv <tool> <install> <--python> <3.14> <nixie-cli==1.1.0>",
]


@pytest.mark.parametrize(
("include_cargo", "include_uv", "expected_error"),
[
(False, True, "cargo is required to install merman-cli"),
(True, False, "uv is required to install nixie-cli"),
],
ids=["missing-cargo", "missing-uv"],
)
def test_install_script_reports_missing_prerequisite(
tmp_path: Path,
*,
include_cargo: bool,
include_uv: bool,
expected_error: str,
) -> None:
"""Missing runner prerequisites should produce actionable errors."""
result = _run_install_script(
tmp_path,
binstall_available=False,
include_cargo=include_cargo,
include_uv=include_uv,
)

assert result.returncode == 1
assert expected_error in result.stderr
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ lib-cov

# Coverage directory used by tools like istanbul
coverage
.coverage
*.lcov

# nyc test coverage
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ GitHub Actions
| 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 |
| Install Nixie | `.github/actions/install-nixie` | unreleased |
| Linux packages | `.github/actions/linux-packages` | v1 |
| macOS package | `.github/actions/macos-package` | v1 |
| Ratchet coverage | `.github/actions/ratchet-coverage` | v1 |
Expand Down
Loading