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
9 changes: 9 additions & 0 deletions .github/actions/install-whitaker/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Changelog

All notable changes to the `install-whitaker` action will be documented in
this file.

## v1.0.0 (Unreleased)

- Add cached installation of the Whitaker Dylint suite
- Prefer cargo-binstall with a `cargo install` fallback
40 changes: 40 additions & 0 deletions .github/actions/install-whitaker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Install Whitaker

Install the Whitaker Dylint suite with a cached `whitaker-installer`.

The action restores the installer and cargo-binstall cache before installation.
When the installer is not cached, it prefers `cargo binstall` and falls back to
`cargo install` when cargo-binstall is unavailable. It then runs
`whitaker-installer` to install the suite.

## Inputs

| Name | Description | Required | Default |
| ------------------- | ------------------------------------------ | -------- | ------- |
| `installer-version` | Version of `whitaker-installer` to install | no | `0.2.6` |

## Outputs

This action has no outputs.
Comment on lines +16 to +18

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

Document outputs in the required table format.

Replace the prose statement with an outputs table, including a row that states that no outputs are exposed. As per coding guidelines, each action README must include “input and output tables”.

Triage: [type:docstyle]

🤖 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-whitaker/README.md around lines 16 - 18, Replace the
prose under the Outputs section with the required outputs table format,
including a row explicitly stating that this action exposes no outputs. Keep the
existing README structure and document style otherwise unchanged.

Sources: Coding guidelines, Path instructions


## Usage

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

- name: Install Whitaker
uses: leynos/shared-actions/.github/actions/install-whitaker@v1
Comment on lines +22 to +27

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

Use the mandated local action reference in the usage example.

Replace the repository-qualified reference with uses: ./.github/actions/install-whitaker@v1. As per coding guidelines, each action README must include “a usage example using uses: ./.github/actions/<name>@<major>”.

Triage: [type:docstyle]

🤖 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-whitaker/README.md around lines 22 - 27, Update the
Install Whitaker usage example to use the mandated local action reference
`./.github/actions/install-whitaker@v1` instead of the repository-qualified
path, while preserving the surrounding setup steps.

Sources: Coding guidelines, Path instructions


- name: Lint
run: make lint
```

The runner must have Cargo available. If `cargo binstall --version` succeeds,
the action installs the requested version with `cargo binstall --locked`.
Otherwise, it builds the same version from crates.io with
`cargo install --locked`.

## Release history

See the [changelog](CHANGELOG.md).
36 changes: 36 additions & 0 deletions .github/actions/install-whitaker/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Install Whitaker
description: Install the Whitaker Dylint suite with a cached installer

inputs:
installer-version:
description: Version of whitaker-installer to install
required: false
default: "0.2.6"

runs:
using: composite
steps:
- name: Cache Whitaker installer
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/bin/whitaker-installer
~/.cache/cargo-binstall
key: >-
whitaker-installer-${{ runner.os }}-${{ runner.arch }}-${{ inputs.installer-version }}

- name: Install Whitaker Dylint suite
shell: bash
env:
WHITAKER_INSTALLER_VERSION: ${{ inputs.installer-version }}
run: |
set -euo pipefail
if ! command -v whitaker-installer >/dev/null 2>&1; then
if cargo binstall --version >/dev/null 2>&1; then
cargo binstall --no-confirm --locked "whitaker-installer@${WHITAKER_INSTALLER_VERSION}"
else
echo "cargo-binstall unavailable; building whitaker-installer from crates.io"
cargo install --locked whitaker-installer --version "${WHITAKER_INSTALLER_VERSION}"
fi
fi
whitaker-installer
Comment on lines +28 to +36

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 Cargo’s bin directory before resolving the installer.

Add ${CARGO_HOME:-$HOME/.cargo}/bin to PATH before command -v and the final invocation. A runner can provide cargo without exposing ~/.cargo/bin, causing both a restored installer and a newly installed installer to remain undiscoverable.

Proposed fix
         set -euo pipefail
+        export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:${PATH}"
         if ! command -v whitaker-installer >/dev/null 2>&1; then
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if ! command -v whitaker-installer >/dev/null 2>&1; then
if cargo binstall --version >/dev/null 2>&1; then
cargo binstall --no-confirm --locked "whitaker-installer@${WHITAKER_INSTALLER_VERSION}"
else
echo "cargo-binstall unavailable; building whitaker-installer from crates.io"
cargo install --locked whitaker-installer --version "${WHITAKER_INSTALLER_VERSION}"
fi
fi
whitaker-installer
set -euo pipefail
export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:${PATH}"
if ! command -v whitaker-installer >/dev/null 2>&1; then
if cargo binstall --version >/dev/null 2>&1; then
cargo binstall --no-confirm --locked "whitaker-installer@${WHITAKER_INSTALLER_VERSION}"
else
echo "cargo-binstall unavailable; building whitaker-installer from crates.io"
cargo install --locked whitaker-installer --version "${WHITAKER_INSTALLER_VERSION}"
fi
fi
whitaker-installer
🤖 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-whitaker/action.yml around lines 28 - 36, Update the
installer setup around the command -v whitaker-installer check to prepend
${CARGO_HOME:-$HOME/.cargo}/bin to PATH before resolving or invoking
whitaker-installer. Preserve the existing installation fallback logic and ensure
both restored and newly installed binaries are discoverable by the final
whitaker-installer command.

174 changes: 174 additions & 0 deletions .github/actions/install-whitaker/tests/test_action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
"""Contract tests for the install-whitaker 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_manifest() -> dict[str, object]:
"""Load the action manifest."""
return typ.cast(
"dict[str, object]",
yaml.safe_load(ACTION_PATH.read_text(encoding="utf-8")),
)


def _install_script() -> str:
"""Return the suite installation shell fragment."""
manifest = _load_manifest()
runs = manifest["runs"]
assert isinstance(runs, dict)
steps = typ.cast("list[dict[str, object]]", runs["steps"])
step = next(
(item for item in steps if item.get("name") == "Install Whitaker Dylint suite"),
None,
)
assert step is not None
script = step.get("run")
assert isinstance(script, str)
return script


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


def _write_cargo_stub(bin_dir: Path) -> None:
"""Write a Cargo stub that records and simulates installer commands."""
_write_executable(
bin_dir / "cargo",
"""#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$*" >> "$CARGO_LOG"
if [ "${1:-}" = "binstall" ] && [ "${2:-}" = "--version" ]; then
[ "$BINSTALL_AVAILABLE" = "true" ]
exit
fi
cat > "$FAKE_BIN_DIR/whitaker-installer" <<'INSTALLER'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "suite installed" >> "$INSTALLER_LOG"
INSTALLER
chmod +x "$FAKE_BIN_DIR/whitaker-installer"
""",
)


def _run_install_script(
tmp_path: Path,
*,
binstall_available: bool,
installer_present: bool = False,
) -> subprocess.CompletedProcess[str]:
"""Run the installation fragment with deterministic command stubs."""
bash = shutil.which("bash")
if bash is None:
pytest.skip("bash not found on PATH")

bin_dir = tmp_path / "bin"
bin_dir.mkdir()
cargo_log = tmp_path / "cargo.log"
installer_log = tmp_path / "installer.log"
_write_cargo_stub(bin_dir)
if installer_present:
_write_executable(
bin_dir / "whitaker-installer",
"""#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "suite installed" >> "$INSTALLER_LOG"
""",
)

env = {
**os.environ,
"PATH": f"{bin_dir}{os.pathsep}/usr/bin{os.pathsep}/bin",
"BINSTALL_AVAILABLE": str(binstall_available).lower(),
"CARGO_LOG": cargo_log.as_posix(),
"FAKE_BIN_DIR": bin_dir.as_posix(),
"INSTALLER_LOG": installer_log.as_posix(),
"WHITAKER_INSTALLER_VERSION": "0.2.6",
}
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 +102 to +109

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

Declare the intentional non-raising subprocess behaviour.

Set check=False because these tests inspect returncode directly. This resolves the reported Ruff warning without changing the failure assertions.

Proposed fix
         capture_output=True,
+        check=False,
         text=True,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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,
)
return subprocess.run( # noqa: S603,TID251 - exercise the Bash fragment.
[bash, "-c", _install_script()],
cwd=tmp_path,
env=env,
capture_output=True,
check=False,
text=True,
timeout=30,
)
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 102-102: 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-whitaker/tests/test_action.py around lines 102 -
109, Update the subprocess.run call in the test helper to explicitly pass
check=False, preserving the existing returncode-based assertions and all other
invocation options.

Source: Linters/SAST tools



def test_manifest_exposes_version_and_cache_contract() -> None:
"""The manifest should expose the pin and cache the installer artefacts."""
manifest = _load_manifest()

assert manifest["inputs"] == {
"installer-version": {
"description": "Version of whitaker-installer to install",
"required": False,
"default": "0.2.6",
}
}
runs = manifest["runs"]
assert isinstance(runs, dict)
steps = typ.cast("list[dict[str, object]]", runs["steps"])
cache_step = steps[0]
assert cache_step["uses"] == (
"actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9"
)
cache_config = typ.cast("dict[str, str]", cache_step["with"])
assert "~/.cargo/bin/whitaker-installer" in cache_config["path"]
assert "~/.cache/cargo-binstall" in cache_config["path"]
assert "${{ inputs.installer-version }}" in cache_config["key"]


def test_installs_with_cargo_binstall_when_available(tmp_path: Path) -> None:
"""cargo-binstall should be preferred when its subcommand is available."""
result = _run_install_script(tmp_path, binstall_available=True)

assert result.returncode == 0, result.stderr
assert (tmp_path / "cargo.log").read_text(encoding="utf-8").splitlines() == [
"binstall --version",
"binstall --no-confirm --locked whitaker-installer@0.2.6",
]
assert (tmp_path / "installer.log").read_text(encoding="utf-8") == (
"suite installed\n"
)


def test_falls_back_to_cargo_install(tmp_path: Path) -> None:
"""Cargo should build whitaker-installer when cargo-binstall is unavailable."""
result = _run_install_script(tmp_path, binstall_available=False)

assert result.returncode == 0, result.stderr
assert (tmp_path / "cargo.log").read_text(encoding="utf-8").splitlines() == [
"binstall --version",
"install --locked whitaker-installer --version 0.2.6",
]
assert "cargo-binstall unavailable" in result.stdout


def test_reuses_cached_installer(tmp_path: Path) -> None:
"""A restored installer should avoid both Cargo installation paths."""
result = _run_install_script(
tmp_path,
binstall_available=False,
installer_present=True,
)

assert result.returncode == 0, result.stderr
assert not (tmp_path / "cargo.log").exists()
assert (tmp_path / "installer.log").read_text(encoding="utf-8") == (
"suite installed\n"
)
Comment on lines +136 to +174

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

Add failure-path contract tests.

Extend the stubs to fail cargo binstall, cargo install, and whitaker-installer, then assert that the action exits non-zero with actionable stderr. Current tests cover only successful acquisition and reuse. As per coding guidelines, action tests “must cover the happy path, common edge cases, and failure modes with clear error messages”.

🤖 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-whitaker/tests/test_action.py around lines 136 -
174, Add failure-path coverage alongside
test_installs_with_cargo_binstall_when_available,
test_falls_back_to_cargo_install, and test_reuses_cached_installer by extending
the command stubs to fail cargo binstall, cargo install, and whitaker-installer
independently. Assert each scenario exits non-zero and writes actionable stderr,
while preserving the existing success and cached-installer contracts.

Sources: Coding guidelines, Learnings

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ __pycache__/
venv/
.env.*
.pytest_cache/
.coverage
.mypy_cache/
*.egg-info/

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 Whitaker | `.github/actions/install-whitaker` | unreleased |

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

Publish the v1 label or omit this unreleased action from the catalogue.

Replace unreleased with the published latest major version once released; otherwise remove the row until publication. The table must list published actions and their latest major versions.

Triage: [type:docstyle]

🧰 Tools
🪛 LanguageTool

[uncategorized] ~17-~17: The official name of this software platform is spelled with a capital “H”.
Context: ... | | Install Whitaker | .github/actions/install-whitaker ...

(GITHUB)

🤖 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 `@README.md` at line 17, Update the “Install Whitaker” catalogue row to show
its published latest major version instead of “unreleased” once v1 is available;
if it remains unpublished, remove the row. Keep the table limited to published
actions and their latest major versions.

Sources: Coding guidelines, Path instructions

| Linux packages | `.github/actions/linux-packages` | v1 |
| macOS package | `.github/actions/macos-package` | v1 |
| Ratchet coverage | `.github/actions/ratchet-coverage` | v1 |
Expand Down
Loading