From f036d4e68e383db7dd57ca8058597f1516b08c56 Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Fri, 2 Oct 2026 12:17:25 -0700 Subject: [PATCH] feat(digest): surface standards-check warnings in the digest Add a "Standards warnings" section to the daily digest issue. For each repository the owner's app installation can read, standards.sh finds the latest standards-check run that vetted the default branch and lists its warning- and notice-level annotations (repo, file, message). Fleet callers trigger standards-check on pull_request only, so a squash merge leaves the default-branch head with no run. standards.sh checks the head commit first, then follows commits/{sha}/pulls to the merged PR that produced it and reads the run on that PR's head commit. Only the Checks, Contents and Pull requests permissions the app already holds are used. Every repository yields one record. A refused read, a nulled check-run list, an annotation count short of the run's own count, a repo with no run, a head with no merged PR, and an unlistable owner each render by name under "Not checked", so "no warnings" can never stand in for "could not check". Standards failures do not block the queue report. Closes #179 --- scripts/dependabot-digest/render.sh | 98 +++++++++- scripts/dependabot-digest/run-digest.sh | 14 +- scripts/dependabot-digest/standards.sh | 184 ++++++++++++++++++ .../installation_repositories.json.o | 52 +++++ .../installation_repositories.json.p | 12 ++ ...pos_o_clean_check-runs_20_annotations.json | 1 + ...bbbbbbbbbbbbbbbbbbbbbbbbbb_check-runs.json | 18 ++ .../standards/repos_o_clean_commits_main.json | 3 + ...ffffffffffffffffffffffffff_check-runs.json | 4 + ...fffffffffffffffffffffffffffffff_pulls.json | 1 + .../repos_o_direct_commits_main.json | 3 + ...c1c1c1c1c1c1c1c1c1c1c1c1c1_check-runs.json | 18 ++ ...cccccccccccccccccccccccccc_check-runs.json | 4 + ...ccccccccccccccccccccccccccccccc_pulls.json | 10 + .../standards/repos_o_norun_commits_main.json | 3 + ...dddddddddddddddddddddddddd_check-runs.json | 4 + .../repos_o_nulled_commits_main.json | 3 + ...pos_o_short_check-runs_50_annotations.json | 24 +++ ...eeeeeeeeeeeeeeeeeeeeeeeeee_check-runs.json | 18 ++ .../standards/repos_o_short_commits_main.json | 3 + ...os_o_warned_check-runs_11_annotations.json | 46 +++++ ...a1a1a1a1a1a1a1a1a1a1a1a1a1_check-runs.json | 44 +++++ ...aaaaaaaaaaaaaaaaaaaaaaaaaa_check-runs.json | 18 ++ ...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_pulls.json | 10 + .../repos_o_warned_commits_main.json | 3 + ...s_p_pwarned_check-runs_70_annotations.json | 14 ++ ...11111111111111111111111111_check-runs.json | 4 + ...1111111111111111111111111111111_pulls.json | 10 + ...12121212121212121212121212_check-runs.json | 18 ++ .../repos_p_pwarned_commits_main.json | 3 + .../tests/test-run-digest.sh | 13 ++ .../dependabot-digest/tests/test-standards.sh | 168 ++++++++++++++++ 32 files changed, 825 insertions(+), 3 deletions(-) create mode 100755 scripts/dependabot-digest/standards.sh create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/installation_repositories.json.o create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/installation_repositories.json.p create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_clean_check-runs_20_annotations.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_clean_commits_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_check-runs.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_clean_commits_main.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_direct_commits_ffffffffffffffffffffffffffffffffffffffff_check-runs.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_direct_commits_ffffffffffffffffffffffffffffffffffffffff_pulls.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_direct_commits_main.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_norun_commits_c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1_check-runs.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_norun_commits_cccccccccccccccccccccccccccccccccccccccc_check-runs.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_norun_commits_cccccccccccccccccccccccccccccccccccccccc_pulls.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_norun_commits_main.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_nulled_commits_dddddddddddddddddddddddddddddddddddddddd_check-runs.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_nulled_commits_main.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_short_check-runs_50_annotations.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_short_commits_eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee_check-runs.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_short_commits_main.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_check-runs_11_annotations.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_commits_a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1_check-runs.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_commits_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_check-runs.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_commits_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_pulls.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_commits_main.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_check-runs_70_annotations.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_commits_1111111111111111111111111111111111111111_check-runs.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_commits_1111111111111111111111111111111111111111_pulls.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_commits_1212121212121212121212121212121212121212_check-runs.json create mode 100644 scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_commits_main.json create mode 100755 scripts/dependabot-digest/tests/test-standards.sh diff --git a/scripts/dependabot-digest/render.sh b/scripts/dependabot-digest/render.sh index e8f5df1..9922282 100755 --- a/scripts/dependabot-digest/render.sh +++ b/scripts/dependabot-digest/render.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # Render classify.sh output as the Markdown body of the digest issue. # -# Usage: render.sh [--run-url URL] < classified.ndjson > body.md +# Usage: render.sh [--run-url URL] [--standards FILE] < classified.ndjson > body.md +# --standards takes standards.sh output. # # The body leads with what a human must do and states, per PR, the fact that # put it in its bucket. It never says a PR will merge: the merge path runs @@ -11,9 +12,11 @@ set -uo pipefail unset CDPATH run_url="" +standards_file="" while [[ $# -gt 0 ]]; do case "$1" in --run-url) run_url="${2-}"; shift 2 ;; + --standards) standards_file="${2-}"; shift 2 ;; *) echo "render.sh: unknown argument: $1" >&2; exit 2 ;; esac done @@ -118,6 +121,99 @@ else fi fi +# dev-env#179. Keep three outcomes apart: warnings, clean, not checked. +render_standards() { + local file="$1" recs checked clean warned unchecked archived rows + echo "## Standards warnings" + echo + if [[ ! -r "${file}" ]]; then + echo "**Not checked.** The standards survey produced no readable result, so nothing below the Dependabot queue was verified." + echo + return 0 + fi + # jq stops at a malformed line, dropping every repo after it. + if ! jq -e -s 'type == "array"' "${file}" >/dev/null 2>&1; then + echo "**Not checked.** The standards survey output is malformed, so nothing was verified." + echo + return 0 + fi + recs="$(jq -c 'select(type == "object" and (.state | type == "string"))' "${file}" 2>/dev/null)" + if [[ -z "${recs}" ]]; then + echo "**Not checked.** The standards survey returned no repositories, so nothing was verified." + echo + return 0 + fi + echo "Warning- and notice-level annotations from each repository's latest \`standards-check\` run that vetted its default branch. Fleet callers run only on pull requests, so that is usually the run on the head commit of the PR that produced the default-branch head. A PR run lints only the files that PR changed (node-floor always checks the whole repo), so \"no warnings\" means that run was clean, not that the whole repository is." + echo + checked="$(jq -s '[.[] | select(.state == "ok")] | length' <<<"${recs}")" + warned="$(jq -s '[.[] | select(.state == "ok" and (.annotations | length) > 0)] | length' <<<"${recs}")" + clean=$((checked - warned)) + unchecked="$(jq -s '[.[] | select(.state != "ok" and .state != "archived")] | length' <<<"${recs}")" + echo "**${checked} repositories checked**: ${warned} with warnings or notices, ${clean} with none. **${unchecked} not checked.**" + echo + + if [[ "${checked}" -eq 0 ]]; then + # Zero warnings out of zero repositories read is not "no warnings". + echo "**No repository could be checked**, so no warnings were looked for." + echo + elif [[ "${warned}" -eq 0 ]]; then + echo "No warnings: no checked repository's latest run carried a warning or notice annotation." + echo + else + # A failed jq prints nothing; an empty table would read as clean. + if ! rows="$(jq -r ' + def cell: tostring | gsub("[\r\n]+"; " ") | gsub("\\|"; "\\|"); + # GitHub puts file-less annotations under path .github. + def where: if .path == "" or .path == ".github" then "—" + else "`" + .path + (if .line then ":" + (.line | tostring) else "" end) + "`" end; + select(.state == "ok") | . as $r | .annotations[] + | "| " + (if $r.runUrl then "[" + $r.repo + "](" + $r.runUrl + ")" else $r.repo end) + + " | " + .level + " | " + where + " | " + (.message | cell) + " |" + ' <<<"${recs}")" || [[ -z "${rows}" ]]; then + echo "**Warnings found but could not be rendered.** ${warned} repositories carry annotations; see the run log." + echo + else + echo "| Repo | Level | File | Message |" + echo "| --- | --- | --- | --- |" + echo "${rows}" + echo + fi + fi + + if [[ "${unchecked}" -gt 0 ]]; then + echo "### Not checked" + echo + echo "These repositories have no readable standards-check result. Their warnings, if any, are unknown — not absent." + echo + if ! rows="$(jq -r ' + def cell: tostring | gsub("[\r\n]+"; " ") | gsub("\\|"; "\\|"); + def state_name: {"unreadable": "UNREADABLE", "unlisted": "OWNER NOT LISTED", + "no-run": "no standards-check run", "no-pr": "no run, no merged PR", + "in-progress": "run in progress"}[.state] // .state; + select(.state != "ok" and .state != "archived") + | "| " + (.repo // ("all of " + .owner)) + " | " + state_name + ": " + (.detail | cell) + " |" + ' <<<"${recs}")" || [[ -z "${rows}" ]]; then + echo "**${unchecked} repositories were not checked, and the list could not be rendered.** See the run log." + echo + else + echo "| Repo | Why |" + echo "| --- | --- |" + echo "${rows}" + echo + fi + fi + + archived="$(jq -rs '[.[] | select(.state == "archived") | .repo] | join(", ")' <<<"${recs}")" + if [[ -n "${archived}" ]]; then + echo "Archived, not surveyed: ${archived}." + echo + fi +} + +if [[ -n "${standards_file}" ]]; then + render_standards "${standards_file}" +fi + echo "---" echo generated_at="$(date -u '+%Y-%m-%d %H:%M UTC')" diff --git a/scripts/dependabot-digest/run-digest.sh b/scripts/dependabot-digest/run-digest.sh index 7d873dd..f7c121c 100755 --- a/scripts/dependabot-digest/run-digest.sh +++ b/scripts/dependabot-digest/run-digest.sh @@ -45,6 +45,8 @@ work="$(mktemp -d)" trap 'rm -rf "${work}"' EXIT collected="${work}/collected.ndjson" : >"${collected}" +standards="${work}/standards.ndjson" +: >"${standards}" issue_token="${GH_TOKEN-}" @@ -67,6 +69,14 @@ for owner in ${OWNERS}; do echo "run-digest.sh: collection failed for ${owner}; not publishing a partial digest" >&2 exit 1 fi + + # Standards never block the queue report. A crash renders as not checked. + echo "run-digest.sh: reading standards-check annotations for ${owner}" >&2 + if ! GH_TOKEN="${token}" bash "${HERE}/standards.sh" "${owner}" >>"${standards}"; then + jq -cn --arg o "${owner}" '{owner: $o, repo: null, state: "unlisted", + detail: "standards.sh failed; see the run log", source: null, + runUrl: null, annotations: []}' >>"${standards}" + fi done classified="${work}/classified.ndjson" @@ -76,8 +86,8 @@ if ! bash "${HERE}/classify.sh" <"${collected}" >"${classified}"; then fi body="${work}/body.md" -render_args=() -[[ -n "${run_url}" ]] && render_args=(--run-url "${run_url}") +render_args=(--standards "${standards}") +[[ -n "${run_url}" ]] && render_args+=(--run-url "${run_url}") if ! bash "${HERE}/render.sh" "${render_args[@]}" <"${classified}" >"${body}"; then echo "run-digest.sh: rendering failed" >&2 exit 1 diff --git a/scripts/dependabot-digest/standards.sh b/scripts/dependabot-digest/standards.sh new file mode 100755 index 0000000..5571e15 --- /dev/null +++ b/scripts/dependabot-digest/standards.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +# Usage: standards.sh . Prints one JSON record per repo: standards-check +# annotations, or the reason none were read. +set -uo pipefail +unset CDPATH + +owner="${1-}" +if [[ -z "${owner}" ]]; then + echo "standards.sh: usage: standards.sh " >&2 + exit 2 +fi + +# Match the reusable job name only: callers may name their job differently. +read -r -d '' pick_run <<'JQ' +[.check_runs[] + | select(.app.slug == "github-actions") + | select(.name == "run-standards-check" + or (.name | endswith(" / run-standards-check")))] +| sort_by(.id) | last +JQ + +# Nulled entries or a short page mean the list cannot be trusted. +read -r -d '' runs_sane <<'JQ' +(.check_runs | type == "array") +and (.total_count | type == "number") +and (.total_count <= (.check_runs | length)) +and all(.check_runs[]; (.id | type == "number") and (.name | type == "string")) +JQ + +emit() { + # emit [source] [run_url] [annotations_json] + local line + line="$(jq -cn --arg owner "${owner}" --arg repo "$1" --arg state "$2" \ + --arg detail "$3" --arg source "${4-}" --arg url "${5-}" \ + --argjson ann "${6:-[]}" \ + '{owner: $owner, repo: (if $repo == "" then null else $repo end), + state: $state, detail: $detail, + source: (if $source == "" then null else $source end), + runUrl: (if $url == "" then null else $url end), + annotations: $ann}' 2>/dev/null)" + # If jq fails, the repo must still appear. + if [[ -z "${line}" ]]; then + printf '{"owner":"%s","repo":"%s","state":"unreadable","detail":"could not encode the result","source":null,"runUrl":null,"annotations":[]}\n' \ + "${owner}" "$1" + return 0 + fi + printf '%s\n' "${line}" +} + +# Callers run api in a subshell, so the failure reason goes to a file. +ERRF="$(mktemp)" +trap 'rm -f "${ERRF}"' EXIT +api() { + : >"${ERRF}" + gh api "$@" 2>"${ERRF}" +} +# emit_err [source] [run_url] +emit_err() { + local why + why="$(last_err "$4")" + emit "$1" "$2" "$3: ${why}" "${5-}" "${6-}" +} +set_err() { printf '%s' "$1" >"${ERRF}"; } +last_err() { + local e + e="$(tr '\n' ' ' <"${ERRF}" | cut -c1-160)" + printf '%s' "${e:-$1}" +} + +# Prints the commit's standards-check run, or nothing. Returns 1 if unreadable. +run_on_commit() { + local nwo="$1" sha="$2" body + if ! body="$(api "repos/${nwo}/commits/${sha}/check-runs?per_page=100")"; then + return 1 + fi + if ! jq -e "${runs_sane}" >/dev/null 2>&1 <<<"${body}"; then + set_err "check-run list for ${sha:0:7} is incomplete or nulled" + return 1 + fi + jq -c "${pick_run} // empty" <<<"${body}" +} + +survey_repo() { + local nwo="$1" branch="$2" head pulls pr pr_head run source ann count url + if ! head="$(api "repos/${nwo}/commits/${branch}" --jq '.sha')" \ + || [[ ! "${head}" =~ ^[0-9a-f]{40}$ ]]; then + emit_err "${nwo}" unreadable "cannot read the head of ${branch}" "no sha returned" + return 0 + fi + + if ! run="$(run_on_commit "${nwo}" "${head}")"; then + emit_err "${nwo}" unreadable "cannot read check runs on ${branch} head ${head:0:7}" "no detail" + return 0 + fi + source="${branch} head ${head:0:7}" + + if [[ -z "${run}" ]]; then + if ! pulls="$(api "repos/${nwo}/commits/${head}/pulls")" \ + || ! jq -e 'type == "array"' >/dev/null 2>&1 <<<"${pulls}"; then + emit_err "${nwo}" unreadable "cannot read the pull request for ${branch} head ${head:0:7}" "non-array body" + return 0 + fi + # Only the PR whose merge produced this commit vetted it. + pr="$(jq -c --arg sha "${head}" '[.[] | select(.merge_commit_sha == $sha and .merged_at != null)] | first // empty' <<<"${pulls}" 2>/dev/null)" + if [[ -z "${pr}" ]]; then + emit "${nwo}" no-pr "${branch} head ${head:0:7} has no standards-check run and was not produced by a merged pull request" "${source}" + return 0 + fi + pr_head="$(jq -r '.head.sha // empty' <<<"${pr}")" + source="#$(jq -r '.number' <<<"${pr}"), merged as ${branch} head ${head:0:7}" + if [[ ! "${pr_head}" =~ ^[0-9a-f]{40}$ ]]; then + emit "${nwo}" unreadable "pull request for ${head:0:7} has no readable head commit" "${source}" + return 0 + fi + if ! run="$(run_on_commit "${nwo}" "${pr_head}")"; then + emit_err "${nwo}" unreadable "cannot read check runs on PR head ${pr_head:0:7}" "no detail" "${source}" + return 0 + fi + if [[ -z "${run}" ]]; then + emit "${nwo}" no-run "no standards-check run on ${branch} head ${head:0:7} or on the head of the PR that produced it" "${source}" + return 0 + fi + fi + + url="$(jq -r '.html_url // ""' <<<"${run}")" + local status run_id + status="$(jq -r '.status' <<<"${run}")" + run_id="$(jq -r '.id' <<<"${run}")" + if [[ "${status}" != "completed" ]]; then + emit "${nwo}" in-progress "latest run has not completed, so its annotations are partial" "${source}" "${url}" + return 0 + fi + + count="$(jq -r '.output.annotations_count // empty' <<<"${run}")" + if [[ ! "${count}" =~ ^[0-9]+$ ]]; then + emit "${nwo}" unreadable "run reports no annotation count" "${source}" "${url}" + return 0 + fi + if ! ann="$(api --paginate --slurp "repos/${nwo}/check-runs/${run_id}/annotations?per_page=100")"; then + emit_err "${nwo}" unreadable "cannot read annotations" "no detail" "${source}" "${url}" + return 0 + fi + # Fewer annotations than the run reports means some were withheld. + ann="$(jq -c --argjson want "${count}" ' + (add // []) as $all + | if all($all[]; (.annotation_level | type == "string") and (.message | type == "string")) + and ($all | length) >= $want + then [$all[] | select(.annotation_level == "warning" or .annotation_level == "notice") + | {level: .annotation_level, path: (.path // ""), + line: (.start_line // null), message: .message}] + else "short" end' <<<"${ann}" 2>/dev/null)" + if [[ -z "${ann}" ]] || ! jq -e 'type == "array"' >/dev/null 2>&1 <<<"${ann}"; then + emit "${nwo}" unreadable "annotations are incomplete or malformed (run reports ${count})" "${source}" "${url}" + return 0 + fi + emit "${nwo}" ok "" "${source}" "${url}" "${ann}" +} + +# The installation list is exactly the set of repos this token can read. +if ! repos="$(api --paginate --slurp "installation/repositories?per_page=100")" \ + || ! repo_list="$(jq -r ' + if type == "array" and all(.[]; (.repositories | type == "array")) + and ([.[].repositories[]] | length) >= (.[0].total_count // 0) + then [.[].repositories[]] | sort_by(.full_name)[] + | [.full_name, (.default_branch // ""), (.archived | tostring)] | @tsv + else error("bad shape") end' <<<"${repos}" 2>/dev/null)"; then + emit_err "" unlisted "cannot list the repositories this token can read" "unexpected response shape" + exit 0 +fi +if [[ -z "${repo_list}" ]]; then + emit "" unlisted "the token can read no repositories, so nothing was checked" + exit 0 +fi + +while IFS=$'\t' read -r nwo branch archived; do + [[ -z "${nwo}" ]] && continue + if [[ "${archived}" == "true" ]]; then + emit "${nwo}" archived "archived; not surveyed" + elif [[ -z "${branch}" ]]; then + emit "${nwo}" unreadable "no default branch reported" + else + survey_repo "${nwo}" "${branch}" + fi +done <<<"${repo_list}" diff --git a/scripts/dependabot-digest/tests/fixtures/standards/installation_repositories.json.o b/scripts/dependabot-digest/tests/fixtures/standards/installation_repositories.json.o new file mode 100644 index 0000000..cb68267 --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/installation_repositories.json.o @@ -0,0 +1,52 @@ +[ + { + "total_count": 8, + "repositories": [ + { + "full_name": "o/warned", + "default_branch": "main", + "archived": false + }, + { + "full_name": "o/clean", + "default_branch": "main", + "archived": false + }, + { + "full_name": "o/norun", + "default_branch": "main", + "archived": false + }, + { + "full_name": "o/nulled", + "default_branch": "main", + "archived": false + } + ] + }, + { + "total_count": 8, + "repositories": [ + { + "full_name": "o/short", + "default_branch": "main", + "archived": false + }, + { + "full_name": "o/direct", + "default_branch": "main", + "archived": false + }, + { + "full_name": "o/old", + "default_branch": "main", + "archived": true + }, + { + "full_name": "o/forbidden", + "default_branch": "main", + "archived": false + } + ] + } +] diff --git a/scripts/dependabot-digest/tests/fixtures/standards/installation_repositories.json.p b/scripts/dependabot-digest/tests/fixtures/standards/installation_repositories.json.p new file mode 100644 index 0000000..8f4142a --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/installation_repositories.json.p @@ -0,0 +1,12 @@ +[ + { + "total_count": 1, + "repositories": [ + { + "full_name": "p/pwarned", + "default_branch": "main", + "archived": false + } + ] + } +] diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_clean_check-runs_20_annotations.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_clean_check-runs_20_annotations.json new file mode 100644 index 0000000..1721244 --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_clean_check-runs_20_annotations.json @@ -0,0 +1 @@ +[[]] diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_clean_commits_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_check-runs.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_clean_commits_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_check-runs.json new file mode 100644 index 0000000..5c1c462 --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_clean_commits_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_check-runs.json @@ -0,0 +1,18 @@ +{ + "total_count": 1, + "check_runs": [ + { + "id": 20, + "name": "lint / run-standards-check", + "status": "completed", + "conclusion": "success", + "app": { + "slug": "github-actions" + }, + "html_url": "https://github.com/x/runs/20", + "output": { + "annotations_count": 0 + } + } + ] +} diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_clean_commits_main.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_clean_commits_main.json new file mode 100644 index 0000000..d30fa79 --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_clean_commits_main.json @@ -0,0 +1,3 @@ +{ + "sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +} diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_direct_commits_ffffffffffffffffffffffffffffffffffffffff_check-runs.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_direct_commits_ffffffffffffffffffffffffffffffffffffffff_check-runs.json new file mode 100644 index 0000000..4c6d61d --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_direct_commits_ffffffffffffffffffffffffffffffffffffffff_check-runs.json @@ -0,0 +1,4 @@ +{ + "total_count": 0, + "check_runs": [] +} diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_direct_commits_ffffffffffffffffffffffffffffffffffffffff_pulls.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_direct_commits_ffffffffffffffffffffffffffffffffffffffff_pulls.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_direct_commits_ffffffffffffffffffffffffffffffffffffffff_pulls.json @@ -0,0 +1 @@ +[] diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_direct_commits_main.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_direct_commits_main.json new file mode 100644 index 0000000..1557e1f --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_direct_commits_main.json @@ -0,0 +1,3 @@ +{ + "sha": "ffffffffffffffffffffffffffffffffffffffff" +} diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_norun_commits_c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1_check-runs.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_norun_commits_c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1_check-runs.json new file mode 100644 index 0000000..8b75f15 --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_norun_commits_c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1_check-runs.json @@ -0,0 +1,18 @@ +{ + "total_count": 1, + "check_runs": [ + { + "id": 30, + "name": "tests", + "status": "completed", + "conclusion": "success", + "app": { + "slug": "github-actions" + }, + "html_url": "https://github.com/x/runs/30", + "output": { + "annotations_count": 0 + } + } + ] +} diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_norun_commits_cccccccccccccccccccccccccccccccccccccccc_check-runs.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_norun_commits_cccccccccccccccccccccccccccccccccccccccc_check-runs.json new file mode 100644 index 0000000..4c6d61d --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_norun_commits_cccccccccccccccccccccccccccccccccccccccc_check-runs.json @@ -0,0 +1,4 @@ +{ + "total_count": 0, + "check_runs": [] +} diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_norun_commits_cccccccccccccccccccccccccccccccccccccccc_pulls.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_norun_commits_cccccccccccccccccccccccccccccccccccccccc_pulls.json new file mode 100644 index 0000000..131da3e --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_norun_commits_cccccccccccccccccccccccccccccccccccccccc_pulls.json @@ -0,0 +1,10 @@ +[ + { + "number": 3, + "merge_commit_sha": "cccccccccccccccccccccccccccccccccccccccc", + "merged_at": "2026-10-01T00:00:00Z", + "head": { + "sha": "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1" + } + } +] diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_norun_commits_main.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_norun_commits_main.json new file mode 100644 index 0000000..d0b1182 --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_norun_commits_main.json @@ -0,0 +1,3 @@ +{ + "sha": "cccccccccccccccccccccccccccccccccccccccc" +} diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_nulled_commits_dddddddddddddddddddddddddddddddddddddddd_check-runs.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_nulled_commits_dddddddddddddddddddddddddddddddddddddddd_check-runs.json new file mode 100644 index 0000000..ccafb65 --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_nulled_commits_dddddddddddddddddddddddddddddddddddddddd_check-runs.json @@ -0,0 +1,4 @@ +{ + "total_count": 2, + "check_runs": [{}, {}] +} diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_nulled_commits_main.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_nulled_commits_main.json new file mode 100644 index 0000000..c5fd171 --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_nulled_commits_main.json @@ -0,0 +1,3 @@ +{ + "sha": "dddddddddddddddddddddddddddddddddddddddd" +} diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_short_check-runs_50_annotations.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_short_check-runs_50_annotations.json new file mode 100644 index 0000000..c53a5a5 --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_short_check-runs_50_annotations.json @@ -0,0 +1,24 @@ +[ + [ + { + "path": "a.yml", + "blob_href": "x", + "start_line": 1, + "end_line": 1, + "annotation_level": "warning", + "title": "", + "message": "one", + "raw_details": "" + }, + { + "path": "b.yml", + "blob_href": "x", + "start_line": 2, + "end_line": 2, + "annotation_level": "notice", + "title": "", + "message": "two", + "raw_details": "" + } + ] +] diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_short_commits_eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee_check-runs.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_short_commits_eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee_check-runs.json new file mode 100644 index 0000000..91b62cc --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_short_commits_eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee_check-runs.json @@ -0,0 +1,18 @@ +{ + "total_count": 1, + "check_runs": [ + { + "id": 50, + "name": "standards-check / run-standards-check", + "status": "completed", + "conclusion": "success", + "app": { + "slug": "github-actions" + }, + "html_url": "https://github.com/x/runs/50", + "output": { + "annotations_count": 5 + } + } + ] +} diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_short_commits_main.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_short_commits_main.json new file mode 100644 index 0000000..1042a62 --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_short_commits_main.json @@ -0,0 +1,3 @@ +{ + "sha": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +} diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_check-runs_11_annotations.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_check-runs_11_annotations.json new file mode 100644 index 0000000..a434ab1 --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_check-runs_11_annotations.json @@ -0,0 +1,46 @@ +[ + [ + { + "path": ".github/workflows/ci.yml", + "blob_href": "x", + "start_line": 12, + "end_line": 12, + "annotation_level": "warning", + "title": "", + "message": "node-version 18 is below the floor 22", + "raw_details": "" + }, + { + "path": ".github", + "blob_href": "x", + "start_line": 51, + "end_line": 51, + "annotation_level": "notice", + "title": "", + "message": "no shell files", + "raw_details": "" + } + ], + [ + { + "path": "scripts/x.sh", + "blob_href": "x", + "start_line": 3, + "end_line": 3, + "annotation_level": "failure", + "title": "", + "message": "SC2086: Double quote to prevent globbing | splitting", + "raw_details": "" + }, + { + "path": ".github/workflows/build.yml", + "blob_href": "x", + "start_line": 7, + "end_line": 7, + "annotation_level": "notice", + "title": "", + "message": "node-version is an expression (${{ inputs.node }}); not checked", + "raw_details": "" + } + ] +] diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_commits_a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1_check-runs.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_commits_a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1_check-runs.json new file mode 100644 index 0000000..702bcee --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_commits_a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1_check-runs.json @@ -0,0 +1,44 @@ +{ + "total_count": 3, + "check_runs": [ + { + "id": 10, + "name": "standards-check / run-standards-check", + "status": "completed", + "conclusion": "success", + "app": { + "slug": "github-actions" + }, + "html_url": "https://github.com/x/runs/10", + "output": { + "annotations_count": 9 + } + }, + { + "id": 11, + "name": "standards-check / run-standards-check", + "status": "completed", + "conclusion": "success", + "app": { + "slug": "github-actions" + }, + "html_url": "https://github.com/x/runs/11", + "output": { + "annotations_count": 4 + } + }, + { + "id": 12, + "name": "tests", + "status": "completed", + "conclusion": "success", + "app": { + "slug": "github-actions" + }, + "html_url": "https://github.com/x/runs/12", + "output": { + "annotations_count": 0 + } + } + ] +} diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_commits_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_check-runs.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_commits_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_check-runs.json new file mode 100644 index 0000000..5256fd9 --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_commits_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_check-runs.json @@ -0,0 +1,18 @@ +{ + "total_count": 1, + "check_runs": [ + { + "id": 5, + "name": "tests", + "status": "completed", + "conclusion": "success", + "app": { + "slug": "github-actions" + }, + "html_url": "https://github.com/x/runs/5", + "output": { + "annotations_count": 0 + } + } + ] +} diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_commits_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_pulls.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_commits_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_pulls.json new file mode 100644 index 0000000..7ca6685 --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_commits_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_pulls.json @@ -0,0 +1,10 @@ +[ + { + "number": 7, + "merge_commit_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "merged_at": "2026-10-01T00:00:00Z", + "head": { + "sha": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1" + } + } +] diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_commits_main.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_commits_main.json new file mode 100644 index 0000000..f19526e --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_o_warned_commits_main.json @@ -0,0 +1,3 @@ +{ + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +} diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_check-runs_70_annotations.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_check-runs_70_annotations.json new file mode 100644 index 0000000..4c5a5bb --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_check-runs_70_annotations.json @@ -0,0 +1,14 @@ +[ + [ + { + "path": ".github", + "blob_href": "x", + "start_line": 1, + "end_line": 1, + "annotation_level": "notice", + "title": "", + "message": "The ubuntu-latest label will migrate", + "raw_details": "" + } + ] +] diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_commits_1111111111111111111111111111111111111111_check-runs.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_commits_1111111111111111111111111111111111111111_check-runs.json new file mode 100644 index 0000000..4c6d61d --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_commits_1111111111111111111111111111111111111111_check-runs.json @@ -0,0 +1,4 @@ +{ + "total_count": 0, + "check_runs": [] +} diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_commits_1111111111111111111111111111111111111111_pulls.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_commits_1111111111111111111111111111111111111111_pulls.json new file mode 100644 index 0000000..0bd905e --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_commits_1111111111111111111111111111111111111111_pulls.json @@ -0,0 +1,10 @@ +[ + { + "number": 2, + "merge_commit_sha": "1111111111111111111111111111111111111111", + "merged_at": "2026-10-01T00:00:00Z", + "head": { + "sha": "1212121212121212121212121212121212121212" + } + } +] diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_commits_1212121212121212121212121212121212121212_check-runs.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_commits_1212121212121212121212121212121212121212_check-runs.json new file mode 100644 index 0000000..88041c7 --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_commits_1212121212121212121212121212121212121212_check-runs.json @@ -0,0 +1,18 @@ +{ + "total_count": 1, + "check_runs": [ + { + "id": 70, + "name": "standards-check / run-standards-check", + "status": "completed", + "conclusion": "success", + "app": { + "slug": "github-actions" + }, + "html_url": "https://github.com/x/runs/70", + "output": { + "annotations_count": 1 + } + } + ] +} diff --git a/scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_commits_main.json b/scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_commits_main.json new file mode 100644 index 0000000..7a66373 --- /dev/null +++ b/scripts/dependabot-digest/tests/fixtures/standards/repos_p_pwarned_commits_main.json @@ -0,0 +1,3 @@ +{ + "sha": "1111111111111111111111111111111111111111" +} diff --git a/scripts/dependabot-digest/tests/test-run-digest.sh b/scripts/dependabot-digest/tests/test-run-digest.sh index c14a6f4..a9a64fb 100755 --- a/scripts/dependabot-digest/tests/test-run-digest.sh +++ b/scripts/dependabot-digest/tests/test-run-digest.sh @@ -161,6 +161,19 @@ else _pass "a populated queue is not reported as empty" fi +# dev-env#179: the section always appears. An unlistable owner is not checked. +if grep -q '^## Standards warnings' <<<"${populated}"; then + _pass "the digest carries a standards warnings section" +else + _fail "the digest has no standards warnings section" +fi +if grep -q '| all of one | OWNER NOT LISTED:' <<<"${populated}" \ + && ! grep -q 'No warnings' <<<"${populated}"; then + _pass "an owner whose repositories cannot be listed renders as not checked" +else + _fail "an unlistable owner did not render as not checked" +fi + if [[ "${fail}" -eq 0 ]]; then echo "test-run-digest: all assertions passed" else diff --git a/scripts/dependabot-digest/tests/test-standards.sh b/scripts/dependabot-digest/tests/test-standards.sh new file mode 100755 index 0000000..3d03d9b --- /dev/null +++ b/scripts/dependabot-digest/tests/test-standards.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# dev-env#179: the section must never read "no warnings" when it could not tell. +set -uo pipefail +unset CDPATH +unset BASH_ENV GH_TOKEN GH_HOST GITHUB_TOKEN + +HERE="$(CDPATH='' cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DIR="${HERE}/.." +FIX="${HERE}/fixtures/standards" +WORK="/tmp/dd-standards-test-$$" +BIN="${WORK}/bin" +mkdir -p "${BIN}" +trap 'rm -rf "${WORK}"' EXIT + +fail=0 +_pass() { echo " PASS: $1"; } +_fail() { echo " FAIL: $1" >&2; fail=1; } + +# Stub gh api: fixtures/standards/.json. No fixture means HTTP 403. +cat >"${BIN}/gh" <<'STUB' +#!/usr/bin/env bash +[[ "$1" == "api" ]] || { echo '[]'; exit 0; } +shift +path="" expr="" +while [[ $# -gt 0 ]]; do + case "$1" in + --jq) expr="$2"; shift 2 ;; + --*) shift ;; + *) path="$1"; shift ;; + esac +done +# The gh wrapper on PATH asks for the current identity before forwarding any +# command, as test-run-digest.sh notes; answer it. +[[ "${path}" == "user" ]] && { echo 'twistedmelonman'; exit 0; } +path="${path%%\?*}" +file="${STUB_FIX}/${path//\//_}.json" +[[ "${path}" == "installation/repositories" ]] && file="${file}.${STUB_OWNER}" +if [[ ! -f "${file}" ]]; then + echo "gh: Resource not accessible by integration (HTTP 403)" >&2 + exit 1 +fi +if [[ -n "${expr}" ]]; then jq -r "${expr}" "${file}"; else cat "${file}"; fi +STUB +chmod +x "${BIN}/gh" + +survey() { + PATH="${BIN}:${PATH}" STUB_FIX="${FIX}" STUB_OWNER="$1" GH_TOKEN=t \ + bash "${DIR}/standards.sh" "$1" +} + +out="${WORK}/standards.ndjson" +: >"${out}" +for o in o p q; do + if ! survey "${o}" >>"${out}" 2>"${WORK}/${o}.err"; then + _fail "standards.sh exited non-zero for owner ${o}; per-repo failures must be records, not crashes" + fi +done + +state_of() { jq -r --arg r "$1" 'select(.repo == $r) | .state' "${out}"; } +_state() { + local got + got="$(state_of "$1")" + if [[ "${got}" == "$2" ]]; then _pass "$3"; else _fail "$3 (state: '${got}', want '$2')"; fi +} + +# Every listed repository must yield exactly one record. +o_count="$(jq -s '[.[] | select(.owner == "o")] | length' "${out}")" +if [[ "${o_count}" == "8" ]]; then + _pass "every listed repo yields one record (8 of 8, across two pages)" +else + _fail "owner o yielded ${o_count} records for 8 listed repos" +fi + +_state o/warned ok "a merged PR's run is found when the default-branch head has none" +_state o/clean ok "a run on the default-branch head itself is used directly" +_state o/norun no-run "a repo with no standards-check run is reported, not dropped" +_state o/nulled unreadable "nulled check-run entries are unreadable, not 'no run'" +_state o/short unreadable "fewer annotations than the run reports is unreadable, not clean" +_state o/direct no-pr "a direct-push head with no run is reported" +_state o/old archived "an archived repo is named as skipped" +_state o/forbidden unreadable "a refused read is unreadable" + +# Re-runs leave several runs on one commit. Only the latest counts. +warned_ann="$(jq -c 'select(.repo == "o/warned") | [.annotations[].message]' "${out}")" +if [[ "${warned_ann}" == *"below the floor"* && "${warned_ann}" == *"no shell files"* ]]; then + _pass "warning and notice annotations are both kept" +else + _fail "expected warning and notice annotations, got ${warned_ann}" +fi +if [[ "${warned_ann}" == *"SC2086"* ]]; then + _fail "a failure-level annotation was listed as a warning" +else + _pass "failure-level annotations are excluded" +fi +if grep -q 'forbidden.*HTTP 403' "${out}"; then + _pass "the reason a read failed is kept in the record" +else + _fail "the refused read lost its reason" +fi +q_rec="$(jq -c 'select(.owner == "q")' "${out}")" +q_shape="$(jq -r '.state + " " + (.repo | tostring)' <<<"${q_rec}")" +if [[ "${q_shape}" == "unlisted null" ]]; then + _pass "an owner whose repos cannot be listed yields one unlisted record" +else + _fail "an unlistable owner was not reported: ${q_rec}" +fi + +# Render the section from the survey. +body="${WORK}/body.md" +: | bash "${DIR}/render.sh" --standards "${out}" >"${body}" 2>"${WORK}/render.err" +_has() { if grep -qF -- "$1" "$3"; then _pass "$2"; else _fail "$2 (missing: $1)"; fi; } +_hasnt() { if grep -qF -- "$1" "$3"; then _fail "$2 (present: $1)"; else _pass "$2"; fi; } + +_has '## Standards warnings' "the section is rendered" "${body}" +_has '**3 repositories checked**: 2 with warnings or notices, 1 with none. **6 not checked.**' \ + "the summary counts checked, warned, clean and not-checked repos" "${body}" +_has "| [o/warned](https://github.com/x/runs/11) | warning | \`.github/workflows/ci.yml:12\` | node-version 18 is below the floor 22 |" \ + "a warning row names repo, level, file and message, linked to the run" "${body}" +_has '| [p/pwarned](https://github.com/x/runs/70) | notice |' \ + "notices from a second owner are listed" "${body}" +_has '### Not checked' "unreadable repos get their own subsection" "${body}" +_has '| o/nulled | UNREADABLE:' "a nulled run is shown as unreadable" "${body}" +_has '| o/norun | no standards-check run:' "a repo with no run is shown by name" "${body}" +_has '| all of q | OWNER NOT LISTED:' "an unlistable owner is shown" "${body}" +_has 'Archived, not surveyed: o/old.' "archived repos are named" "${body}" +_has 'not that the whole repository is' "the scope limit of a PR run is stated" "${body}" +_hasnt 'No warnings:' "a section with warnings does not claim none" "${body}" + +# All clean: the empty state must be an explicit statement. +clean="${WORK}/clean.ndjson" +jq -c 'select(.repo == "o/clean")' "${out}" >"${clean}" +: | bash "${DIR}/render.sh" --standards "${clean}" >"${WORK}/clean.md" 2>/dev/null +_has 'No warnings: no checked repository' "a clean survey says so explicitly" "${WORK}/clean.md" +_hasnt '### Not checked' "a fully read survey has no not-checked list" "${WORK}/clean.md" + +# Clean repos alongside an unreadable one: "no warnings" must not hide the gap. +mixed="${WORK}/mixed.ndjson" +jq -c 'select(.repo == "o/clean" or .repo == "o/nulled")' "${out}" >"${mixed}" +: | bash "${DIR}/render.sh" --standards "${mixed}" >"${WORK}/mixed.md" 2>/dev/null +_has '**1 not checked.**' "not-checked repos are counted next to a clean result" "${WORK}/mixed.md" +_has '| o/nulled | UNREADABLE:' "not-checked repos are listed next to a clean result" "${WORK}/mixed.md" + +# Every repo unreadable: zero warnings from zero reads is not "no warnings". +jq -c 'select(.repo == "o/nulled")' "${out}" >"${WORK}/allbad.ndjson" +: | bash "${DIR}/render.sh" --standards "${WORK}/allbad.ndjson" >"${WORK}/allbad.md" 2>/dev/null +_has '**No repository could be checked**' "an all-unreadable survey says nothing was checked" "${WORK}/allbad.md" +_hasnt 'No warnings' "an all-unreadable survey never claims no warnings" "${WORK}/allbad.md" + +# A survey that produced nothing, or garbage, is not a clean survey. +: >"${WORK}/empty.ndjson" +: | bash "${DIR}/render.sh" --standards "${WORK}/empty.ndjson" >"${WORK}/empty.md" 2>/dev/null +_has '**Not checked.**' "an empty survey renders as not checked" "${WORK}/empty.md" +_hasnt 'No warnings' "an empty survey never claims no warnings" "${WORK}/empty.md" +printf '%s\n' '{"owner":"o","repo":"o/clean","state":"ok","annotations":[]}' '{not json' >"${WORK}/bad.ndjson" +: | bash "${DIR}/render.sh" --standards "${WORK}/bad.ndjson" >"${WORK}/bad.md" 2>/dev/null +_has 'output is malformed' "a malformed survey renders as not checked" "${WORK}/bad.md" +_hasnt 'No warnings' "a malformed survey never claims no warnings" "${WORK}/bad.md" + +# Without --standards there was no survey, and the section must not appear. +: | bash "${DIR}/render.sh" >"${WORK}/none.md" 2>/dev/null +_hasnt '## Standards warnings' "no survey, no section" "${WORK}/none.md" + +if [[ "${fail}" -eq 0 ]]; then + echo "test-standards: all assertions passed" +else + echo "test-standards: FAILURES" +fi +exit "${fail}"