#!/usr/bin/env bats # Guards the "Downstream Repositories" trigger map in docs/agents/contributing.md. # # The map tells a contributor which change in this repo forces a follow-up in a # satellite repository. Each trigger names a file on this side: hack/update-crd.sh, # packages/core/platform/values.yaml, and so on. Rename one of those and the map # keeps confidently pointing at a path that no longer exists, from inside the PR # template, to every reader. The repository list is deliberately not restated here # — it would be one more copy to drift; the map and the PR template are the two # sources, and the tests below hold them to each other. # # The three tests pin, in order: every in-repo path the map cites to a file that # exists; the workflow guarantee that Bats contracts run on every docs-only PR; # and the map and the PR-template checklist to the same repository list, so a # repository cannot be listed in one and forgotten in the other. # # WHAT THIS DOES NOT CHECK, so nobody mistakes a green tick for a correct map: # # * Whether a trigger is TRUE. "Change X here and Y breaks over there" is a # claim about a satellite repo. Verifying it needs that repo checked out at # a known ref, which this suite deliberately does not do: a unit test must # not depend on the network or on six sibling clones. A trigger can cite a # path that exists here and still describe a coupling that is nonsense. # * Anything on the satellite side. A file named there (its Makefile, its # scripts/, its playbooks) is not reachable from this repo and is not # validated. Those names are exactly where the map is most likely to rot. # * Whether the map is COMPLETE. Deleting a whole trigger, or a whole block of # them, passes: the repository-list test compares section headings, not bodies. # * A path written without backticks. Extraction keys off `code spans`, which # is the section's convention; a lone unquoted path slips through. Stripping # the backticks wholesale does fail, via the anti-vacuum guard below. # # In short: this catches a rename on THIS side, which is the cheap half. The # expensive half — is the coupling real, and is it still real over there — stays # a human's job, and reviewers should not assume CI did it for them. # # A docs-only PR skips builds and E2E, but the `checks` job still runs the Bats # lane. Otherwise editing this map, or another document audited by a unit # contract, would exempt the changed input from its own guard. load test_helper REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)" MAP_FILE="$REPO_ROOT/docs/agents/contributing.md" TEMPLATE_FILE="$REPO_ROOT/.github/PULL_REQUEST_TEMPLATE.md" # Body of the "## Downstream Repositories" section, up to the next h2. map_section() { awk '/^## Downstream Repositories$/ { inside = 1; next } /^## / { inside = 0 } inside' "$MAP_FILE" } @test "every in-repo path cited by the downstream trigger map exists" { [ -f "$MAP_FILE" ] || { echo "missing $MAP_FILE" >&2; exit 1; } section="$(map_section)" [ -n "$section" ] || { echo "Could not locate the '## Downstream Repositories' section in $MAP_FILE" >&2; exit 1; } # Backticked tokens rooted at a top-level directory of THIS repo. Satellite # paths cited by the map (scripts/package.mk, content/en/docs/...) do not match # these roots and so fall out. Note this is a heuristic, not a guarantee: a # satellite repo has its own packages/ tree, so a backticked path from over # there that happens to share a root would be checked against this tree. # A trailing slash is prose, not a distinct path: `packages/apps/` and # `packages/apps` are the same anchor and would otherwise both inflate the count. paths="$(printf '%s\n' "$section" \ | grep -o '`[^`]*`' \ | tr -d '`' \ | grep -E '^(hack|packages|cmd|api|internal|pkg|\.github)/' \ | sed 's#/$##' \ | sort -u)" # Guard against a vacuous pass: if the extraction breaks, or the section is # reworded so it no longer cites paths, this test must fail loudly rather than # silently verify nothing. The floor sits well under the real count (28 as of # writing) because the map legitimately shrinks too, and a red build should not # send the reader hunting for a bug that is really a deleted section. count="$(printf '%s\n' "$paths" | grep -c . || true)" [ "$count" -ge 20 ] || { echo "Extracted only $count in-repo path(s) from the trigger map; expected at least 20." >&2 echo "Either the extraction broke and this test is verifying nothing, or the map genuinely" >&2 echo "shrank - if a downstream repository went away, lower this floor deliberately." >&2 exit 1 } missing="" for path in $paths; do # A placeholder segment () stands for any package, so match it as a glob. pattern="$(printf '%s' "$path" | sed 's/<[^>]*>/*/g')" # Unquoted on purpose: the shell must expand the glob. No match => ls fails. if ! (cd "$REPO_ROOT" && ls -d $pattern) >/dev/null 2>&1; then missing="$missing $path" fi done if [ -n "$missing" ]; then echo "The downstream trigger map in docs/agents/contributing.md cites paths that do not exist:" >&2 for path in $missing; do echo " - $path" >&2; done echo "Fix: update the map to the new path, or drop the trigger if it no longer applies." >&2 echo "The map is read by contributors from inside the PR template; a stale path there sends them to a dead end." >&2 exit 1 fi echo "All $count in-repo path(s) cited by the trigger map exist" } @test "the workflow runs Bats contracts for docs-only changes" { WORKFLOW="$REPO_ROOT/.github/workflows/pull-requests.yaml" [ -f "$WORKFLOW" ] || { echo "missing $WORKFLOW" >&2; exit 1; } job_if=$(yq -r '.jobs.checks.if' "$WORKFLOW") code_if=$(yq -r '.jobs.checks.steps[] | select(.name == "Run unit and controller tests") | .if' "$WORKFLOW") docs_if=$(yq -r '.jobs.checks.steps[] | select(.name == "Run Bats unit tests for docs-only changes") | .if' "$WORKFLOW") docs_run=$(yq -r '.jobs.checks.steps[] | select(.name == "Run Bats unit tests for docs-only changes") | .run' "$WORKFLOW") code_run=$(yq -r '.jobs.checks.steps[] | select(.name == "Run unit and controller tests") | .run' "$WORKFLOW") [ "$job_if" = "!contains(github.event.pull_request.labels.*.name, 'release')" ] [ "$code_if" = "needs.plan.outputs.code == 'true'" ] [ "$docs_if" = "needs.plan.outputs.code == 'false'" ] [ "$docs_run" = 'make bats-unit-tests' ] [ "$code_run" = 'make unit-tests test-controllers test-controllers-envtest -j4 -k --output-sync=target' ] } @test "the trigger map and the PR-template checklist list the same repositories" { [ -f "$TEMPLATE_FILE" ] || { echo "missing $TEMPLATE_FILE" >&2; exit 1; } # Checklist lines look like: - [ ] [cozystack/website](https://...) - follow-up: template_repos="$(grep -oE '^- \[ \] \[cozystack/[a-z0-9.-]+\]' "$TEMPLATE_FILE" \ | grep -oE 'cozystack/[a-z0-9.-]+' \ | sort -u)" # Map headings look like: ### cozystack/website map_repos="$(map_section \ | grep -oE '^### cozystack/[a-z0-9.-]+' \ | grep -oE 'cozystack/[a-z0-9.-]+' \ | sort -u)" template_count="$(printf '%s\n' "$template_repos" | grep -c . || true)" [ "$template_count" -ge 1 ] || { echo "Found no cozystack/* checklist entries in $TEMPLATE_FILE - extraction is broken" >&2; exit 1; } if [ "$template_repos" != "$map_repos" ]; then echo "The PR-template checklist and the trigger map disagree on which repositories are downstream." >&2 echo "In the checklist (.github/PULL_REQUEST_TEMPLATE.md):" >&2 printf '%s\n' "$template_repos" | sed 's/^/ /' >&2 echo "In the map (docs/agents/contributing.md, '### cozystack/' headings):" >&2 printf '%s\n' "$map_repos" | sed 's/^/ /' >&2 echo "Fix: add the repository to both, or remove it from both." >&2 echo "A repository listed in only one of them gets ticked with no guidance, or documented with no box to tick." >&2 exit 1 fi echo "Checklist and trigger map agree on $template_count repositories" }