Skip to content

test(ci): guard the setup-micromamba action SHA, not only its binary version #448

Description

@bioedca

Maintenance kind

test

Work requested

tests/test_marker_contract.py::test_every_setup_micromamba_call_site_pins_the_binary
guards the binary version — it asserts every setup-micromamba call site declares its own
micromamba-version and that all of them equal MICROMAMBA_PIN. It does not guard the
action SHA in the uses: line, so the three call sites may sit on three different versions of
the action itself with CI fully green.

Nothing else is looking. No committed test asserts that a pinned action SHA agrees across call
sites
: this module does read uses: values — _step_uses, and the attestation guards around it —
but for a different property, that one named step exists and can run in one named job, and
f457c30a868e4760d3a6fcea5f25dc655b8edf39 appears nowhere under tests/. This repository also runs
neither actionlint nor zizmor as a committed check; zizmor is named only in a comment in
.github/workflows/sidecar.yml.

The section banner above the guard already promises the property the guard does not deliver:

# --- setup-micromamba: one action version, one binary version, and Dependabot can see both ---

and the trap list under it names precisely this drift as the reason the section exists:

That is how .github/actions/setup-env sat on setup-micromamba v2 while packaging.yml and
release.yml were bumped to v3.0.0: the composite action was invisible to the bot.

Dependabot now sees both directories (that gap was closed), but it sees them through two
github-actions entries in .github/dependabot.yml — group actions on directory: "/", which
for this ecosystem means .github/workflows plus a root-level action.yml, and group
actions-composite on directories: ["/.github/actions/*"]. Two entries means two pull
requests
, so the split re-appears for as long as only one of the pair has merged, and permanently
if one is closed or forgotten.

Codex raised it as a P2 on #446, against the setup-micromamba step in
.github/workflows/packaging.yml: landing #446 without #445 would have left
.github/actions/setup-env/action.yml on v3.1.0 while packaging.yml and release.yml ran v3.2.1.
Seven workflows consume that composite — uses: ./.github/actions/setup-env appears in ci.yml,
schema-guard.yml, sidecar.yml, sidecar-measure.yml, deep.yml, deps-audit.yml and
large-fixtures.yml, which between them provide required CI contexts — and nothing in the
repository would have said so.

Both pull requests did land together — #445 (491b603, the actions-composite group) and #446
(c16ffd2, the actions group) — so the tree is consistent. Verified at origin/main b078f47:
all three call sites read `mamba-org/setup-micromamba@f457c30a868e4760d3a6fcea5f25dc655b8edf39

v3.2.1`. This issue is about the missing guard, not about a live mismatch, which is exactly

why every criterion below carries the mutation that makes it fail — a guard added against an
already-agreeing tree is green from its first commit whether or not it asserts anything.

Branch from a freshly fetched origin/main. A checkout predating #445/#446 sees
ce51e99f4bb8a82ab7158c4dc59ef4634c59c4f9 # v3.1.0 at all three sites; that is still uniform, so
the demonstrations work, but the SHA quoted in the PR must be the one actually on the branch.

The existing helper's discovery glob is reusable; its extraction is not.
_setup_micromamba_call_sites walks GITHUB_DIR.rglob("*.y*ml"), which already visits
.github/actions/setup-env/action.yml as well as the two workflows — that half is free, and all 22
files the glob matches today parse cleanly as YAML mappings. What it does not do is survive
contact with a uses: line. It counts them with

uses = len(re.findall(r"^\s*(?:-\s*)?uses:\s*" + re.escape(SETUP_MICROMAMBA), text, re.M))

and that expression carries both of the false-green modes this module's own _steps() docstring
records against the attestation guard (#319 — the first found by Codex, the second by CodeRabbit).
Measured against this exact expression:

input counts should count
- uses: mamba-org/setup-micromamba@<sha> 1 1
- uses: "mamba-org/setup-micromamba@<sha>" (quoted, and valid YAML) 0 1
the same uses: text inside a run: block scalar 1 0

A SHA assertion built on that extraction is blind to a call site written with quotes and credulous
about one that is only a line of shell. This module already settled the question — parse the
document — and the settled answer needs one adjustment before it can be reused here: _steps()
reads workflow.get("jobs"), and .github/actions/setup-env/action.yml has no jobs: key at all
(its top-level keys are name, description, inputs, runs), so _steps() and _step_uses
see none of the one call site this issue mostly exists for. Discovery has to read steps from
both jobs.*.steps and runs.steps. That is the trap to avoid: a guard that parses, looks correct,
passes review, and never sees the composite action — while len(call_sites) >= 3 keeps passing
because the text-based count is still what feeds it.

One thing a parse cannot do is see a comment — yaml.safe_load discards the trailing # v3.2.1
outright. So the tag-comment half is necessarily read from the file text, and what keeps that text
half honest is requiring its per-file count to equal the parsed call-site count: a comment pattern
that misses a spelling the parser found then fails loudly instead of quietly dropping a site.

The failure message should name the split-Dependabot-PR cause, since that is the situation a
contributor will actually be in when it fires.

On ADR-0064. tests/test_marker_contract.py is not one of "the agent test modules" the freeze
covers. It contains no reference to .agents/, claim.py or reaper.py; its subject is the
sidecar-marker collection glob and the .github/ pin and Dependabot-coverage contracts. The
agent-layer test modules are tests/test_claim.py, tests/test_reaper.py,
tests/test_agent_contract_is_runnable.py, tests/test_agent_entry_points.py and
tests/test_greptile_usage.py. Adding a guard here is ordinary test work, not a capability change
on a frozen layer.

Acceptance criteria

Superseded by the grooming block at the end of this issue, which is authoritative.

Scope and non-goals

In scope: tests/test_marker_contract.py only — _setup_micromamba_call_sites (its extraction,
not its glob), the setup-micromamba guard(s) in that section, the new unit test of the extraction,
and the section's header comment.

Out of scope:

  • bumping any action version;
  • generalising the guard to every repeated uses: SHA across all workflows (a bigger change with
    its own false-positive questions — uses: SHAs are legitimately independent between different
    actions);
  • anything about .github/dependabot.yml, whose directory coverage is already correct and already
    guarded by test_dependabot_watches_every_composite_action and
    test_no_dependabot_glob_overlaps_the_workflows_directory;
  • the stale # v3.0.0 (node24). sentence that opens the narrative comment block above
    uses: mamba-org/setup-micromamba@ in .github/actions/setup-env/action.yml.
    It is a real
    defect of the same family and the new guard cannot see it — the guard reads the trailing comment
    on the uses: line, which correctly says # v3.2.1. It stays out for a measured reason: any edit
    under .github/actions/setup-env/ matches the relevance pattern in .github/workflows/sidecar.yml
    (the pattern='^(src/tether/idealize/|sidecar/|…|\.github/actions/setup-env/)' line), which flips
    the required sidecar / parity context from a seconds-long pass into a full live run — git lfs pull, the sidecar lock restore, the pinned-tMAVEN install and the live fits — on an action that
    fix(ci): setup-micromamba failing intermittently on both OSes since the v3.2.1 bump #452 reports failing intermittently. That is a heavy, flaky price for a comment. It belongs with
    the repository-wide uses: <action>@<40-hex> # <tag> resolver that chore(ci): agent-reaper pins two actions to SHAs its comments misname #437 already reserves for its
    own issue, or in a comment-only issue of its own.

Size estimate

S — up to 150 added lines

The assertions themselves are a handful of lines. The band is set by the rest: converting the
extraction to a parse over two step containers, the unit test of that extraction with its four rows
(house style in this module is a 10–20-line docstring per test), the failure-message text and the
header comment.

Risk level

standard — the lane: Codex on the draft, then CodeRabbit

Does this need an architecture decision record?

no — no schema, dependency-isolation, architectural or scientifically consequential change.

Dependencies / blockers

none

Execution autonomy

agent-can-do-alone

Related work / file overlap

#445 and #446 (the Dependabot pair whose split surfaced this; both merged). #437 is the sibling
instance of the same family — a pin whose comment misnames it — and reserves the repository-wide
comment-resolves-to-tag check for a separate issue. Files: tests/test_marker_contract.py.

Target area

CI / repository governance

Milestone

none / cross-cutting

Required safety attestations

  • I searched existing issues and this is not a duplicate.
  • I am not reporting a vulnerability publicly; security reports use the private advisory flow.
  • I included no secrets or private, raw, unlicensed, user, or lab data.

Grooming status — 2026-08-13

  • Status: unblocked.
  • Open dependencies: none. ci(deps): Bump mamba-org/setup-micromamba from 3.1.0 to 3.2.1 in /.github/actions/setup-env in the actions-composite group across 1 directory #445 merged as 491b603; ci(deps): Bump the actions group with 3 updates #446 merged as c16ffd2.
  • Autonomy: agent-can-do-alone.
  • Why it is agent-doable: the whole diff is one test module; the discovery glob already visits
    every call site; no version is bumped and no third-party code changes.
  • Chosen design — one shared helper, and it parses. _setup_micromamba_call_sites keeps its
    GITHUB_DIR.rglob("*.y*ml") walk and gains the uses: SHA and the trailing # v<X.Y.Z> tag
    alongside the counts it already returns. Per-document extraction moves into a small function that
    takes the document text, so it can be unit-tested directly instead of only through the file
    system. Steps come from both jobs.*.steps and runs.steps of the parsed document; the SHA
    comes from the parsed step's uses value; the trailing tag comes from the file text, because a
    parse discards comments; and the two are cross-checked by count. Do not add a second walk over
    .github/, and do not widen the guard to other actions. The micromamba-version half may
    keep its existing text extraction — the only requirement on it is that its len(pins) != uses
    comparison now compares against the parsed call-site count.
  • Review risk: low. Confined to tests/; the tree already satisfies the new assertions, so the
    only way to know they assert anything is the mutation record required below.
  • Terminal condition: PR-ready handoff. No merge authority is granted here; a maintainer arms
    the merge.

Acceptance criteria

  • _setup_micromamba_call_sites in tests/test_marker_contract.py additionally returns, per
    file, the uses: action SHA and the trailing # v<X.Y.Z> tag comment for every call site it
    finds, and it reads those call sites from the parsed document — steps taken from both
    jobs.*.steps and runs.steps, so .github/actions/setup-env/action.yml (top-level keys
    name, description, inputs, runs; no jobs:) is covered rather than silently skipped.
    Discovery stays the existing GITHUB_DIR.rglob("*.y*ml") walk, so a fourth call site — in a
    workflow or in a new .github/actions/*/action.yml — is covered without editing any file
    list. No second walker is added.

  • A committed test exercises the extraction directly, on document text passed in, in the shape
    of the existing test_the_anchor_guard_recognises_a_step_and_nothing_else. Four rows:
    (a) a plain - uses: mamba-org/setup-micromamba@<40 hex> step counts once; (b) the quoted
    spelling - uses: "mamba-org/setup-micromamba@<40 hex>" — valid YAML, and the exact form that
    made the attestation guard false-green — counts once; (c) the same text inside a run: block
    scalar counts zero; (d) a document whose only top-level keys are name and runs, with
    the call site under runs.steps, counts once.
    Currently-failing demonstration, run and quoted in the PR: do it red-first, in two steps.
    (i) Move today's expression —
    re.findall(r"^\s*(?:-\s*)?uses:\s*" + re.escape(SETUP_MICROMAMBA), text, re.M) — into the new
    text-taking function unchanged, point the new test at it, and run: rows (b) and (c) are red.
    Paste that output, then convert the function to the parse and paste it green. (ii) With the
    parse in place, narrow its step lookup to jobs.*.steps only and confirm row (d) goes red;
    paste that too, and revert. Those three reds are what distinguishes this from a guard that
    parses, reviews cleanly, and still cannot see the composite action.

  • tests/test_marker_contract.py asserts that the set of uses: SHAs across all
    setup-micromamba call sites has exactly one element. The assertion may extend
    test_every_setup_micromamba_call_site_pins_the_binary or live in a sibling test in the same
    section, but it must read the shared helper.
    Currently-failing demonstration, run and quoted in the PR: change the SHA in
    .github/actions/setup-env/action.ymlmamba-org/setup-micromamba@f457c30a868e4760d3a6fcea5f25dc655b8edf39
    to the same action at 40 arbitrary hex digits — run the focused test, paste the red output,
    revert. Repeat at one workflow call site (packaging.yml or release.yml) so both step
    containers are shown to be covered. The composite is the load-bearing half: a jobs:-only
    lookup stays green on that mutation, so a green run there is the signal that the guard is
    blind.

  • The trailing # v<X.Y.Z> comment is asserted to agree across call sites as well, with no
    "or the docstring says why not" alternative — no test decides a docstring.
    Currently-failing demonstration: change one site's # v3.2.1 to # v3.2.0, focused test
    red, revert.

  • A call site whose uses: line carries no trailing # v… comment fails rather than being
    silently skipped: the number of extracted tag comments per file must equal that file's parsed
    call-site count, the same shape as the existing len(pins) != uses check. This is also what
    keeps the text half and the parsed half from disagreeing in silence.
    Currently-failing demonstration: delete the trailing comment on one call site, focused test
    red, revert. A pattern that only collects annotated lines would otherwise pass by dropping the
    unannotated one.

  • The failure message for the SHA assertion names the cause a contributor will be in: the two
    Dependabot pull requests — group actions for .github/workflows, group actions-composite
    for /.github/actions/*, both entries in .github/dependabot.yml — must land together, and
    one merged without the other splits the versions. git grep -n "land together" -- tests/test_marker_contract.py (or the wording actually chosen) locates it.

  • The section header comment above GITHUB_DIR — the block opening
    # --- setup-micromamba: one action version, one binary version, and Dependabot can see both ---
    — records the action-SHA trap alongside the two it already numbers, and its closing sentence,
    currently # These guards keep both closed: …, is rewritten so it covers the third trap rather
    than the two it currently names.

  • Local gates green: pytest tests/test_marker_contract.py first, then pre-commit run --all-files and the test-matrix line for your lane from AGENTS.md §Local gates before
    review. No docs or schema change, so mkdocs build --strict and scripts/dump_schema.py --check are not required — say so in the PR rather than leaving it ambiguous.

  • The diff touches tests/test_marker_contract.py and nothing else:
    git diff --name-only origin/main...HEAD prints exactly that one path. Every mutation above is
    local, reverted, and never committed — which is also what keeps .github/actions/setup-env/
    out of the diff and the required sidecar / parity context off its full live run. In
    particular no workflow, no composite action and no .github/dependabot.yml edit.

  • Ready condition: met; awaiting a fresh exact-scope marker.

Metadata

Metadata

Assignees

No one assigned

    Labels

    risk:standardDeclared risk: standard. Routes nothing - every PR walks the same lane. May only increase.size:SDiff budget <=150 changed lines (excl. lockfiles/generated)status:readyReadytype:testTests / fixtures

    Projects

    Status
    No status

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions