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
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def test_action_install_step_resolves_from_external_checkout(tmp_path: Path) ->
"target",
"_self",
".cache",
".hypothesis",
".uv-cache",
),
)
Expand Down
19 changes: 13 additions & 6 deletions .github/workflows/mutation-cargo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,16 @@ jobs:
persist-credentials: false

- name: Setup Rust
if: ${{ steps.workflow-source.outputs.checkout == 'true' }}
uses: ./workflow-src/.github/actions/setup-rust
if: ${{ env.ACT != 'true' }}
# Reference the composite action from its own repository, pinned by
# SHA, rather than the workspace-relative `./workflow-src/...` path.
# A remote `uses:` is materialised under the runner's managed
# `_actions` directory, which lives outside the caller's workspace
# and survives job teardown, so setup-rust's cache-save post hook can
# still resolve `action.yml` after "Relocate workflow source" has
# moved workflow-src to $RUNNER_TEMP (issue #365). Bump this pin in
# step with the resolve-workflow-source pin above.
uses: leynos/shared-actions/.github/actions/setup-rust@62733fd653cc577f9858645d9a3e4daf90998c55
Comment on lines +206 to +215

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

Update the stale relocation comment.

Update the later comment at Line 263. The setup-rust step now uses the remote action from the runner-managed _actions directory, so the statement that Setup Rust needs the workspace-local action path is false. Remove that rationale or replace it with the actual ordering requirement.

🤖 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/workflows/mutation-cargo.yml around lines 206 - 215, Update the
comment near the Setup Rust step, using the setup-rust action reference and the
surrounding workflow ordering as context. Remove the obsolete claim that Setup
Rust requires a workspace-local action path, and describe only the actual
ordering requirement if one remains.


- name: Install cargo-mutants
if: ${{ env.ACT != 'true' }}
Expand Down Expand Up @@ -252,10 +260,9 @@ jobs:
# tree-scanning tests in the caller's suite (manifest sweeps,
# file inventories, lint-everything globs) never see foreign
# files during the mutation baseline or mutant runs. Runs after
# "Setup Rust" (which needs the workspace-local action path) and
# "Setup uv" (whose cache-dependency-glob hashes the workflow
# scripts while they sit inside the workspace). Under act the
# workspace itself is the workflow source, so nothing moves.
# "Setup uv", whose cache-dependency-glob hashes the workflow scripts
# while they sit inside the workspace. Under act the workspace itself
# is the workflow source, so nothing moves.
id: relocate-workflow-source
shell: bash
env:
Expand Down
9 changes: 9 additions & 0 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,15 @@ Internals for maintainers:
fail-fast branches are exercised by
`tests/workflows/test_resolve_workflow_source.py`; the OIDC happy path is
validated by every real run of the consuming workflows.
- In `mutation-cargo.yml`, keep `Setup Rust` as a remote
`leynos/shared-actions/.github/actions/setup-rust` reference pinned to a full
SHA. The workflow relocates `workflow-src` before teardown, while GitHub
re-resolves a local `action.yml` for post hooks such as setup-rust's cache
save; the remote reference is materialized outside the caller's workspace
and survives that relocation. The current Setup Rust step is guarded with
`if: ${{ env.ACT != 'true' }}` and ordered before relocation; static
workflow-shape tests enforce those invariants and prohibit `./workflow-src/`
action uses.

## Running the Test Suite

Expand Down
8 changes: 4 additions & 4 deletions docs/execplans/add-mutation-testing-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,10 +275,10 @@ proceeds.
in this repo, keeps helper scripts in lockstep with the caller's pinned
workflow SHA with no extra inputs, and its `ACT` bypass is exactly what the
integration tests need; three copies of a known-good block beat one novel
mechanism. The `setup-rust` step uses the repo's own action via the
checked-out `workflow-src` path
(`uses: ./workflow-src/.github/actions/setup-rust`), skipped under `act`
where stub binaries stand in for the toolchain.
mechanism. The `setup-rust` step uses the remote pinned action
(`leynos/shared-actions/.github/actions/setup-rust@<full SHA>`) under the
`ACT` guard, so its post hook survives relocation of `workflow-src`; Python
helpers can still use the checked-out `workflow-src` path.
- 2026-07-04: The wireframe finding about feature-gated tests (its
issue #571) is addressed by the `extra-args` input (e.g. `--all-features`)
rather than a dedicated features input — cargo- mutants accepts arbitrary
Expand Down
56 changes: 56 additions & 0 deletions workflow_scripts/tests/test_mutation_workflow_shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from __future__ import annotations

import re
import typing as typ
from pathlib import Path

Expand All @@ -25,6 +26,8 @@
CHECKOUT_STEP = "Checkout workflow repository"
RELOCATE_STEP = "Relocate workflow source"
RELOCATED_DIR_EXPR = "${{ steps.relocate-workflow-source.outputs.workflow_dir }}"
LOCAL_WORKFLOW_SRC_PREFIX = "./workflow-src/"
REMOTE_SETUP_RUST_PREFIX = "leynos/shared-actions/.github/actions/setup-rust@"

pytestmark = pytest.mark.skipif(
not all((WORKFLOWS_DIR / name).exists() for name in WORKFLOW_NAMES),
Expand Down Expand Up @@ -55,6 +58,59 @@ def _step_names(steps: list[dict[str, object]]) -> list[object]:
return [step.get("name") for step in steps]


@pytest.mark.parametrize("workflow_name", WORKFLOW_NAMES)
def test_no_step_references_a_relocated_workflow_src_action(
workflow_name: str,
) -> None:
"""No step invokes a composite action via the ``./workflow-src/`` path.

A workspace-relative ``uses: ./workflow-src/...`` reference breaks at
job teardown: the Actions runtime re-resolves the local action's
``action.yml`` from that path when running its post hook, but the
"Relocate workflow source" step has by then moved ``workflow-src`` to
``$RUNNER_TEMP``. The post step fails to find the definition and reddens
an otherwise green run (issue #365). Composite actions with post hooks
must instead be referenced remotely (pinned by SHA); the runner
materialises those under its managed ``_actions`` directory, outside the
caller's workspace and surviving teardown.
"""
for job_name, job in _jobs(workflow_name).items():
for step in _steps(job):
uses = step.get("uses")
if not isinstance(uses, str):
continue
assert not uses.startswith(LOCAL_WORKFLOW_SRC_PREFIX), (
f"{workflow_name}:{job_name} step {step.get('name')!r} "
f"references {uses!r}; a workspace-relative workflow-src "
f"action path breaks its post hook once workflow-src is "
f"relocated to $RUNNER_TEMP (issue #365). Reference the "
f"action remotely, pinned by SHA, instead."
)
return


def test_mutants_setup_rust_survives_workflow_source_relocation() -> None:
"""Setup Rust is remote, SHA-pinned, guarded, and precedes relocation."""
steps = _steps(_jobs("mutation-cargo.yml")["mutants"])
names = _step_names(steps)
assert "Setup Rust" in names, "mutants job must contain a Setup Rust step"
assert RELOCATE_STEP in names, "mutants job must relocate workflow-src"
setup_rust = steps[names.index("Setup Rust")]
uses = setup_rust.get("uses")

assert isinstance(uses, str)
assert re.fullmatch(
rf"{re.escape(REMOTE_SETUP_RUST_PREFIX)}[0-9a-f]{{40}}", uses
), "mutants:Setup Rust must use the remote action pinned by a full SHA"
assert setup_rust.get("if") == "${{ env.ACT != 'true' }}"
assert names.index("Setup Rust") < names.index(RELOCATE_STEP)

relocate = steps[names.index(RELOCATE_STEP)]
run = relocate.get("run")
assert isinstance(run, str)
assert '"${RUNNER_TEMP}/workflow-src"' in run


Comment on lines +61 to +113

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 terminal return.

Add return after the nested loops. The -> None function currently relies on implicit None, which violates the repository's R503 rule.

As per coding guidelines, add an explicit return at the end of every function and avoid relying on implicit None (R503).

Apply the explicit terminal return
@@
             )
+    return
🤖 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 `@workflow_scripts/tests/test_mutation_workflow_shape.py` around lines 59 - 88,
Add an explicit terminal return to
test_no_step_references_a_relocated_workflow_src_action after the nested job and
step loops, preserving the existing assertions and function behavior.

Source: Coding guidelines

@pytest.mark.parametrize("workflow_name", WORKFLOW_NAMES)
def test_every_workflow_checkout_is_followed_by_relocation(
workflow_name: str,
Expand Down
Loading