Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 35s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 57s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 37s
PR Gates / Docs update reminder (pull_request) Successful in 1m0s
PR Gates / decisions lifecycle (pull_request) Successful in 20s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 17s
review-verdict/h10 Review-verdict: MERGEABLE @ a7d91bf (base: main)
Review verdict / Set review-verdict status (pull_request_target) Successful in 45s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m25s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m17s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 19m27s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m4s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
`docs.no-session-narrative` reaches every durable artifact, but its detector scanned only `docs/**/*.md` and root markdown, and nothing had ever swept the rest. The issue named four sites from one grep and called them a floor. Deriving the population instead — a whitespace-joined sweep over every tracked file outside the detector, for the detector's own phrasings plus the attribution and review-round class #812 found — gave 453 sites in 108 files at `fb5592971`, and a second pass for phrasings the first list missed (hyphenated `round-N`, "an earlier version", "the reviewer proved") added residuals in the same files. Every site was classified with #812's three dispositions (CUT / SEVER / KEEP with its sub-kind) under the who-benefits test; the per-site manifests are on the PR. The rejected designs, tested-and-rejected fixtures, measurements and traps stay; the attribution of who found them and the round in which they were found go. The detector's population grows to `.claude/`, `.gitea/`, `.husky/` and `scripts/` regardless of extension, minus the detector and its own test (whose fixtures ARE the phrasings) and minus `scripts/tests/fixtures/` (test data, including decision-record copies — the same reasoning as the records' own exemption, and what keeps the record's depth measurement true), and `--all` lists tracked REGULAR files only — a symlink's content is its target and a gitlink has none. The #812 argument for leaving `docs/superpowers/**` in the population runs the other way here: `--diff` sees only ADDED lines, and 287 of the 453 sites were under 30 days old — this corpus is where narrative is being added, so the advisory nudge has reach. Density agrees: 56 line-mode hits over the 113 regular files the predicate admits, against 9 over 66 docs files before #812. `web/` and C# stay out on the same measurement (3 of 74 PATTERNS-matching sites, ~4,600 files). The predicate did not grow: PATTERNS matched 74 of 453 sites, and widening the word list to the attribution class is the treadmill the withdrawn parity test ran on. The population oracle is restated over segments with the new arms, the synthetic cross product gains the process heads and non-markdown extensions, a fixture witnesses that a tracked symlink is neither scanned nor counted, a `.py.bak` axis separates a by-name exemption from a `startswith` over the same tuple, and eight mutants (drop the process arm, drop the by-name exemption, exempt by `startswith`, drop or add a prefix, drop the fixtures exemption, list only markdown, drop the symlink filter, test the mode per row instead of per path) each redden it. A pre-existing silent drop in `--diff` goes with it: git tab-terminates a `+++` filename that contains a space, and the kept tab made `is_scanned_path` refuse the file with no notice — fixed, with a positive control and its own mutant. Code is unchanged by construction, measured per file type against `origin/main`: Python modules are AST-equal with docstrings stripped, except `#` lines inside the embedded fixture programs (string literals) of three test modules; workflows differ only in `#` lines inside `run:` block scalars; shell, C#, TypeScript and jq are equal with comment lines stripped. The stated exceptions: the detector and its test, 26 vitest titles that carried review-round or severity labels or a reviewer attribution (call sites whose title changed — every changed title line walked back to its `it(` / `it.each(...)(` anchor, so a `' + '` concatenation counts once), two registry note strings and the mutation manifest's prose fields. scripts/tests: 1565 passed. Web: lint, typecheck, 1319 tests green. Closes #876. Decisions-Edit: yes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PEcBoFw7ctrf3Nb7R7x7wk
367 lines
19 KiB
Python
367 lines
19 KiB
Python
"""`scripts/ci-detect-docs-only.sh` must never graft a COMPLETE clone shallow (ersatztv#836).
|
|
|
|
`git fetch --depth=N` writes `.git/shallow` and cuts history at N even when every object is already
|
|
present. Exactly four `docker-build.yml` jobs run the detector — `test`, `migrations` and
|
|
`functional-e2e` at `fetch-depth: 2`, and `build` at `fetch-depth: 0` — so `build` is the only
|
|
complete-clone consumer, and the detector asks the repository which shape it is in rather than
|
|
assuming. (`api-docs` and `format` have a step with `id: detect` too, but it runs their own inline
|
|
diff, not this script. Do not add them here.)
|
|
|
|
WHAT WENT WRONG WITHOUT THIS. `build`'s next step after the detector is `Compute version and tags`,
|
|
whose `git describe --tags --abbrev=0` needs a reachable tag. The graft cut it off, `describe`
|
|
failed, and a `|| echo v0.0.0` fallback that cannot fail turned that into a version string: every
|
|
`:latest` image published from `main` carried `InformationalVersion 0.0.0-<sha>` instead of
|
|
`26.x.y-<sha>`, from 2026-07-17 (#416) until #836, with nothing red anywhere. It was found by
|
|
reading the string out of
|
|
a running container. That is why the assertion here is on the RESULTING VERSION and on the shallow
|
|
flag — not on the detector's exit status, which was `0` throughout the defect.
|
|
|
|
WHY THE FIXTURE USES `file://`. `git clone /path` and `git fetch /path` use the local transport,
|
|
which IGNORES `--depth` outright. A fixture built on a plain path would therefore never graft, every
|
|
assertion below would hold, and the whole file would pass while testing nothing —
|
|
`testing.eight-ways-a-test-passes-for-the-wrong-reason`. `test_the_fixture_can_actually_graft` is the
|
|
negative control that keeps that honest: it performs the raw depth fetch the detector is forbidden to
|
|
perform and REQUIRES the graft to happen. If git or the transport ever stops grafting, that control
|
|
goes red and says so, instead of the other tests quietly becoming vacuous.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
DETECTOR = REPO_ROOT / "scripts" / "ci-detect-docs-only.sh"
|
|
|
|
TAG = "v26.3.1"
|
|
|
|
|
|
def _env(**extra: str) -> dict[str, str]:
|
|
"""Ambient `GIT_*` removed, everything else kept.
|
|
|
|
Exported `GIT_DIR`/`GIT_WORK_TREE`/`GIT_INDEX_FILE` override `cwd` and would aim these git calls
|
|
at the real repository. The rest of `os.environ` is carried deliberately rather than built from
|
|
scratch: `conftest.py`'s isolation puts `ETV_HOOK_FIRE_LOG_DIR` there, and a from-scratch
|
|
environment would drop it and write into the production hook-fire log.
|
|
|
|
`GITHUB_` is dropped alongside `GIT_` because it does NOT match the `GIT_` prefix
|
|
(`"GITHUB_OUTPUT".startswith("GIT_")` is False), and every `GITHUB_*` the detector reads decides
|
|
which arm it takes. Pinning only the one variable that bites today would leave the class open for
|
|
the next arm that starts reading a sibling — the whole prefix goes, and `_run_detector` puts back
|
|
exactly what each test means to set.
|
|
|
|
THE MACHINE'S GIT CONFIG IS NEUTRALISED TOO, and that is not belt-and-braces — it is what keeps
|
|
this file from passing here and failing in CI. A developer checkout has a global `user.email`;
|
|
the CI container has none, and git refuses to commit without one ("Author identity unknown").
|
|
The fixtures below commit, so without this the file is green locally and red in CI on
|
|
exactly that. Pointing the global and system config at `os.devnull` makes the ambient
|
|
configuration UNAVAILABLE rather than merely overridden, so a missing `-c` flag in `_git` fails
|
|
everywhere instead of only where the machine is unhelpful — the local run reproduces CI by
|
|
construction. It also disarms a global `core.hooksPath` (which would fire this repo's husky
|
|
hooks against throwaway fixtures) and a global `commit.gpgsign` (which would block on a pinentry
|
|
prompt no pytest timeout is watching).
|
|
"""
|
|
env = {k: v for k, v in os.environ.items() if not k.startswith(("GIT_", "GITHUB_"))}
|
|
env["GIT_CONFIG_GLOBAL"] = os.devnull
|
|
env["GIT_CONFIG_SYSTEM"] = os.devnull
|
|
env.update(extra)
|
|
return env
|
|
|
|
|
|
# Passed to every git invocation, so no repository here can inherit an identity or a hook path from
|
|
# the machine. `_env()` makes the ambient config unavailable; these supply what the commits need.
|
|
_GIT_CONF = [
|
|
"-c",
|
|
"user.email=test@example.com",
|
|
"-c",
|
|
"user.name=Test",
|
|
"-c",
|
|
"commit.gpgsign=false",
|
|
"-c",
|
|
"core.hooksPath=",
|
|
# Forbids git from INVENTING an identity from the OS user and hostname. Without it the two
|
|
# settings above are not enough to reproduce CI: a Mac happily auto-detects one, so dropping the
|
|
# identity flags stays green locally and fails in the container. With it, a missing flag fails
|
|
# everywhere — verified by removing the identity pair and watching this file go red.
|
|
"-c",
|
|
"user.useConfigOnly=true",
|
|
]
|
|
|
|
|
|
def _git(args: list[str], cwd: Path) -> str:
|
|
r = subprocess.run(["git", *_GIT_CONF, *args], cwd=str(cwd), env=_env(), capture_output=True, text=True)
|
|
assert r.returncode == 0, f"git {' '.join(args)} failed: {r.stderr}"
|
|
return r.stdout.strip()
|
|
|
|
|
|
def _git_rc(args: list[str], cwd: Path) -> subprocess.CompletedProcess:
|
|
return subprocess.run(["git", *_GIT_CONF, *args], cwd=str(cwd), env=_env(), capture_output=True, text=True)
|
|
|
|
|
|
def _is_shallow(repo: Path) -> str:
|
|
return _git(["rev-parse", "--is-shallow-repository"], repo)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def origin_url(tmp_path_factory) -> str:
|
|
"""An origin shaped like ours: the newest tag is well back, and the tip is a merge commit.
|
|
|
|
Both properties matter. The tag has to be further back than the fetch depth or the graft cannot
|
|
cut it off, and the tip has to be a merge commit or the detector's push arm reports
|
|
`non-merge push` and returns before reaching anything this file is about.
|
|
"""
|
|
root = tmp_path_factory.mktemp("detector-depth")
|
|
origin = root / "origin"
|
|
origin.mkdir()
|
|
_git(["init", "-q", "-b", "main", "."], origin)
|
|
|
|
for i in range(1, 13):
|
|
(origin / f"f{i}.txt").write_text(f"{i}\n")
|
|
_git(["add", "-A"], origin)
|
|
_git(["commit", "-q", "-m", f"c{i}"], origin)
|
|
_git(["tag", TAG], origin)
|
|
for i in range(13, 31):
|
|
(origin / f"f{i}.txt").write_text(f"{i}\n")
|
|
_git(["add", "-A"], origin)
|
|
_git(["commit", "-q", "-m", f"c{i}"], origin)
|
|
|
|
# A docs-only branch merged with --no-ff, i.e. exactly how an update to `main` lands here.
|
|
_git(["checkout", "-q", "-b", "feature"], origin)
|
|
(origin / "docs").mkdir()
|
|
(origin / "docs" / "a.md").write_text("doc\n")
|
|
_git(["add", "-A"], origin)
|
|
_git(["commit", "-q", "-m", "docs change"], origin)
|
|
_git(["checkout", "-q", "main"], origin)
|
|
_git(["merge", "-q", "--no-ff", "feature", "-m", "merge feature (#1)"], origin)
|
|
|
|
return f"file://{origin}"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def deep_origin_url(tmp_path_factory) -> str:
|
|
"""An origin with MORE than 200 commits after its newest tag, for the PR arm's `--depth=200`.
|
|
|
|
Built separately, and the extra ~3s is the price of a test that is not vacuous. The other
|
|
fixture's 31 commits all fit inside a depth of 200, so a `--depth=200` fetch there has nothing
|
|
to cut: no `.git/shallow` is written, `describe` keeps resolving, and a PR-arm test built on it
|
|
passes against the UNFIXED script — measured, which is how this fixture came to exist. A guard
|
|
whose positive case cannot occur is the defect it is meant to catch, one level up.
|
|
"""
|
|
root = tmp_path_factory.mktemp("detector-depth-deep")
|
|
origin = root / "origin"
|
|
origin.mkdir()
|
|
_git(["init", "-q", "-b", "main", "."], origin)
|
|
(origin / "a.txt").write_text("a\n")
|
|
_git(["add", "-A"], origin)
|
|
_git(["commit", "-q", "-m", "c0"], origin)
|
|
_git(["tag", TAG], origin)
|
|
# Empty commits: the depth boundary counts commits, and 210 real ones would only be slower.
|
|
for i in range(1, 211):
|
|
_git(["commit", "-q", "--allow-empty", "-m", f"c{i}"], origin)
|
|
return f"file://{origin}"
|
|
|
|
|
|
def _clone(origin_url: str, dest: Path, depth: int | None = None) -> Path:
|
|
args = ["clone", "-q"]
|
|
if depth is not None:
|
|
args += [f"--depth={depth}"]
|
|
args += [origin_url, str(dest)]
|
|
_git(args, dest.parent)
|
|
return dest
|
|
|
|
|
|
def _run_detector(repo: Path, tmp_path: Path, **github_env: str) -> tuple[subprocess.CompletedProcess, str]:
|
|
"""Drive the real detector.
|
|
|
|
`GITHUB_REF_TYPE` is pinned to `branch` rather than left to the ambient environment. `_env()`
|
|
deliberately carries the rest of `os.environ` (for `ETV_HOOK_FIRE_LOG_DIR`), and the detector's
|
|
FIRST branch returns early on `GITHUB_REF_TYPE=tag` — before any fetch — so an exported `tag`
|
|
turns every test here red for a reason that has nothing to do with what they assert.
|
|
|
|
Not live today, stated precisely rather than dramatised: the `scan` job DOES run pytest on tag
|
|
pushes, but over two named files, and this is not one of them; `script-tests`, which does run
|
|
this file, is `on: pull_request`. Adding this file to `scan`'s argument list is the one edit that
|
|
would make it live. It is a default (`env.update(github_env)` follows), so a future test can
|
|
still drive the tag arm deliberately.
|
|
"""
|
|
out_file = tmp_path / f"gh-output-{repo.name}"
|
|
out_file.write_text("")
|
|
env = {"GITHUB_REF_TYPE": "branch"}
|
|
env.update(github_env)
|
|
r = subprocess.run(
|
|
["bash", str(DETECTOR)],
|
|
cwd=str(repo),
|
|
env=_env(GITHUB_OUTPUT=str(out_file), **env),
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
return r, out_file.read_text()
|
|
|
|
|
|
# --------------------------------------------------------------------------------------------
|
|
# The negative control. Everything else here asserts that a graft does NOT happen, so it is all
|
|
# vacuous if the fixture cannot graft in the first place.
|
|
# --------------------------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"fixture_name, depth",
|
|
[("origin_url", 2), ("deep_origin_url", 200)],
|
|
ids=["push-arm-depth-2", "pr-arm-depth-200"],
|
|
)
|
|
def test_the_fixture_can_actually_graft(request, fixture_name, depth, tmp_path):
|
|
"""The raw `--depth=N` fetch the detector must not perform DOES graft this fixture.
|
|
|
|
Parametrised over BOTH fixtures and BOTH depths, because a control that covers only one of
|
|
them leaves the other free to go vacuous unreported — and that is MEASURED: the PR arm against
|
|
the 31-commit origin, where a depth of 200 has nothing to cut, passed against the unfixed
|
|
script. One control per (fixture, depth) pair the file relies on.
|
|
"""
|
|
origin = request.getfixturevalue(fixture_name)
|
|
repo = _clone(origin, tmp_path / f"control-{depth}")
|
|
assert _is_shallow(repo) == "false", "a plain clone should be complete"
|
|
assert _git(["describe", "--tags", "--abbrev=0"], repo) == TAG
|
|
|
|
_git(["fetch", "--no-tags", f"--depth={depth}", "origin", "main"], repo)
|
|
|
|
assert _is_shallow(repo) == "true", (
|
|
f"a --depth={depth} fetch did not graft this fixture, so the test that relies on it is "
|
|
"vacuous — most likely the clone/fetch fell back to the local transport, which ignores "
|
|
"--depth (use file://), or the fixture has fewer commits than the depth"
|
|
)
|
|
described = _git_rc(["describe", "--tags", "--abbrev=0"], repo)
|
|
assert described.returncode != 0, (
|
|
"the graft happened but `git describe` still resolved, so the version this file protects "
|
|
"is not actually reachable from the shallow boundary — the fixture's tag is too close to "
|
|
f"the tip. stdout={described.stdout!r}"
|
|
)
|
|
|
|
|
|
# --------------------------------------------------------------------------------------------
|
|
# The guard proper.
|
|
# --------------------------------------------------------------------------------------------
|
|
|
|
|
|
def test_the_push_arm_leaves_a_COMPLETE_clone_complete(origin_url, tmp_path):
|
|
"""`build`'s shape: `fetch-depth: 0`, push to main. The version must survive the detector.
|
|
|
|
This is the proof named by `scripts/ci-detect-docs-only.sh`'s `MUTATION` row in
|
|
`docs/guard-inventory.md`; the declared mutation in `mutation_manifest.py` disarms the shallow
|
|
condition, which puts the `--depth=2` fetch back and reddens the two assertions below.
|
|
"""
|
|
repo = _clone(origin_url, tmp_path / "build-shape")
|
|
assert _is_shallow(repo) == "false"
|
|
|
|
r, output = _run_detector(repo, tmp_path, GITHUB_EVENT_NAME="push", GITHUB_REF_NAME="main")
|
|
assert r.returncode == 0, f"detector failed: {r.stdout}{r.stderr}"
|
|
|
|
assert _is_shallow(repo) == "false", (
|
|
"the detector GRAFTED the complete clone shallow — this is ersatztv#836: the next step in "
|
|
"`build` is `Compute version and tags`, whose `git describe` then finds no reachable tag "
|
|
f"and the image ships InformationalVersion 0.0.0-<sha>.\ndetector said:\n{r.stdout}"
|
|
)
|
|
|
|
described = _git_rc(["describe", "--tags", "--abbrev=0"], repo)
|
|
assert described.returncode == 0 and described.stdout.strip() == TAG, (
|
|
"after the detector, `git describe --tags --abbrev=0` no longer resolves, so `build` would "
|
|
f"stamp this image 0.0.0. git said: {described.stdout.strip()!r} {described.stderr.strip()!r}"
|
|
)
|
|
|
|
# The detector still has to do its actual job on this shape.
|
|
assert "docs_only=true" in output, (
|
|
f"the merge tip is docs-only but the detector did not say so:\n{r.stdout}\n{output}"
|
|
)
|
|
|
|
|
|
def test_the_pull_request_arm_leaves_a_COMPLETE_clone_complete(deep_origin_url, tmp_path):
|
|
"""A complete checkout on the PR arm, N=200. NO PRODUCTION CONSUMER TODAY — deliberately.
|
|
|
|
MEASURED: on `pull_request` the only
|
|
jobs running this script are the three `fetch-depth: 2` ones, so the PR arm's `--depth=200` is
|
|
inert rather than latently firing. What this pins is the SHARED CLAUSE — the next `fetch-depth: 0`
|
|
consumer added to this arm must not silently inherit the graft that cost #836. It is a guard
|
|
against a future shape, not a reproduction of a shipped one, and grading it as the latter would
|
|
overstate what the file proves.
|
|
|
|
The deep fixture is what makes the case real at all: with the 32-commit origin a depth of 200 has
|
|
nothing to cut, and this test passed against the unfixed script.
|
|
"""
|
|
repo = _clone(deep_origin_url, tmp_path / "pr-shape")
|
|
assert _is_shallow(repo) == "false"
|
|
# A PR head: one docs commit on top of the base branch.
|
|
_git(["checkout", "-q", "-b", "pr-branch"], repo)
|
|
(repo / "docs").mkdir(exist_ok=True)
|
|
(repo / "docs" / "b.md").write_text("doc\n")
|
|
_git(["add", "-A"], repo)
|
|
_git(["commit", "-q", "-m", "docs on the PR head"], repo)
|
|
|
|
r, output = _run_detector(repo, tmp_path, GITHUB_EVENT_NAME="pull_request", GITHUB_BASE_REF="main")
|
|
assert r.returncode == 0, f"detector failed: {r.stdout}{r.stderr}"
|
|
|
|
assert _is_shallow(repo) == "false", (
|
|
"the PR arm GRAFTED a complete clone shallow (ersatztv#836). No job runs this arm against a "
|
|
f"complete checkout today, so this is the shared clause regressing.\n{r.stdout}"
|
|
)
|
|
described = _git_rc(["describe", "--tags", "--abbrev=0"], repo)
|
|
assert described.returncode == 0 and described.stdout.strip() == TAG, (
|
|
"after the PR arm, `git describe --tags --abbrev=0` no longer resolves: "
|
|
f"{described.stdout.strip()!r} {described.stderr.strip()!r}"
|
|
)
|
|
# FETCH_HEAD is what the PR arm diffs against, so the depth-less fetch must still write it.
|
|
assert _git_rc(["rev-parse", "--verify", "-q", "FETCH_HEAD"], repo).returncode == 0, (
|
|
"the PR arm no longer writes FETCH_HEAD, so its base revision is unresolvable"
|
|
)
|
|
assert "docs_only=true" in output, (
|
|
f"the PR head is docs-only but the detector did not say so:\n{r.stdout}\n{output}"
|
|
)
|
|
|
|
|
|
def test_a_SHALLOW_checkout_still_gets_its_depth_and_still_decides(origin_url, tmp_path):
|
|
"""The other half: `test`/`migrations`/`functional-e2e` at `fetch-depth: 2` must be unchanged.
|
|
|
|
Removing the graft must not turn into removing the depth. A shallow checkout stays shallow, and
|
|
the detector still resolves `HEAD^1` and classifies the docs-only merge — the behaviour the
|
|
`--depth=2` was added for in the first place.
|
|
"""
|
|
repo = _clone(origin_url, tmp_path / "shallow-shape", depth=2)
|
|
assert _is_shallow(repo) == "true", "the fixture clone was meant to be shallow"
|
|
|
|
r, output = _run_detector(repo, tmp_path, GITHUB_EVENT_NAME="push", GITHUB_REF_NAME="main")
|
|
assert r.returncode == 0, f"detector failed: {r.stdout}{r.stderr}"
|
|
|
|
assert _is_shallow(repo) == "true", "a shallow checkout was silently unshallowed"
|
|
assert "docs_only=true" in output, (
|
|
"the shallow checkout no longer classifies the docs-only merge — the depth stopped being "
|
|
f"applied where it IS needed:\n{r.stdout}\n{output}"
|
|
)
|
|
assert "fetching with --depth=2" in r.stdout, f"the shallow checkout did not take the depth branch:\n{r.stdout}"
|
|
assert "fetching without --depth" not in r.stdout, (
|
|
f"the shallow checkout took the complete-clone branch:\n{r.stdout}"
|
|
)
|
|
|
|
|
|
def test_an_UNKNOWN_shallow_answer_passes_no_depth(tmp_path):
|
|
"""Fail-direction, stated and executed: `--is-shallow-repository` unanswerable => no depth.
|
|
|
|
Defaulting the other way would re-graft on exactly the paths nobody can observe, so the branch
|
|
is pinned rather than argued. Running outside a repository is one way to make the query
|
|
unanswerable and the one this drives; it is NOT the only one — on a git older than 2.15 the flag
|
|
is unrecognised and rev-parse echoes it back verbatim with status 0. That shape is not covered
|
|
here (it needs an ancient git), and it reaches the same no-depth branch.
|
|
|
|
WHAT THIS DELIBERATELY DOES NOT ASSERT. Outside a repository the detector exits 128 with an
|
|
empty `$GITHUB_OUTPUT`, because `set -o pipefail` makes the `git rev-list ... | wc -w` assignment
|
|
below the fetch abort under `set -e`. That is PRE-EXISTING and unchanged here — measured against
|
|
`8aeacd534`, the commit this fix branched from, which exits 128 the same way. It is also not a
|
|
fail-open: the step goes red, so the job does, and no `docs_only=true` is ever emitted. Asserting
|
|
a clean exit here would be asserting a behaviour the script has never had.
|
|
"""
|
|
notarepo = tmp_path / "notarepo"
|
|
notarepo.mkdir()
|
|
r, _output = _run_detector(notarepo, tmp_path, GITHUB_EVENT_NAME="push", GITHUB_REF_NAME="main")
|
|
assert "is-shallow=unknown" in r.stdout, f"the unknown answer was not reached or not reported:\n{r.stdout}"
|
|
assert "fetching without --depth" in r.stdout, f"an unknown shallow answer must not ask for a depth:\n{r.stdout}"
|