diff --git a/.github/actions/resolve-config/api-budget-coverage.test.mjs b/.github/actions/resolve-config/api-budget-coverage.test.mjs index 201cf356ba1..60611717605 100644 --- a/.github/actions/resolve-config/api-budget-coverage.test.mjs +++ b/.github/actions/resolve-config/api-budget-coverage.test.mjs @@ -45,6 +45,12 @@ const ALLOWED = new Map([ "intake-triage.yml:triage-split-children", `${NO_LOG}; step "Triage each sub-issue" calls gh without api_budget_wrap_gh`, ], + // issues: write alone, so no checkout to load the action from: one rollup + // listing and at most two writes. + [ + "maintenance-file-dead-links.yml:file", + `${NO_LOG}; step "Rewrite the rollup issue" calls gh without api_budget_wrap_gh`, + ], [ "post-merge-demotion-alert.yml:close-cleared", `${NO_LOG}; step "Close the records main cleared" calls gh without api_budget_wrap_gh`, diff --git a/.github/workflows/maintenance-file-dead-links.yml b/.github/workflows/maintenance-file-dead-links.yml new file mode 100644 index 00000000000..f84b7bfc300 --- /dev/null +++ b/.github/workflows/maintenance-file-dead-links.yml @@ -0,0 +1,238 @@ +name: maintenance-file-dead-links + +# Routes the dead links the weekly online link check finds to one rollup +# issue, so each is repaired by the demotion path in +# plugins/attribution/skills/audit/reference/dispositions.md ("The demotion +# path when a pointer later dies"). This repository owns that trigger; the +# plugin owns the repair. +# +# The link check (maintenance-check-links-online-claude-code-plugins.yml) is a +# sync-managed caller of the ci-workflows reusable workflow, which uploads the +# run's broken links as the link-check-online-dead-links artifact. A +# reusable workflow's outputs reach only its own caller, so this workflow reads +# the artifact instead, in two jobs that each hold one scope: +# +# - read: actions: read, to download the artifact from the link-check run. It +# checks the shape, keeps at most MAX_LINKS links, and hands them on through +# its outputs, an empty list included. A run that uploaded no artifact (a +# scheduled run the repository property turned off) hands on nothing, so +# the rollup is left as it is. +# - file: issues: write alone, no checkout. It finds the rollup, an issue +# github-actions[bot] filed whose first body line is +# ``, and rewrites its list of dead +# links. With none it closes an open rollup as completed, with a comment. A +# rollup closed as completed is reopened when links are dead again; one +# closed as not planned is left alone. With no rollup it opens one, labeled +# needs-triage: its token starts no workflow, so intake-triage.yml never +# adds that floor label on open, and the hourly triage sweep leaves +# github-actions[bot] issues out (triage-sweep.mjs), so the label is what +# puts the issue in the untriaged queue. A reopened rollup keeps its labels. +# +# The trigger names the link-check workflow by its `name:`, which the +# standards component owns: a rename there disconnects this workflow. + +on: + # Runs this file from the default branch and executes no code from the + # link-check run: the artifact is data, checked before use. + workflow_run: # zizmor: ignore[dangerous-triggers] + workflows: [maintenance-check-links-online-claude-code-plugins] + types: [completed] + branches: [main] + +permissions: {} + +jobs: + read: + name: Read the dead links + if: >- + github.event.workflow_run.head_branch == 'main' + && github.event.workflow_run.head_repository.full_name == github.repository + && contains(fromJSON('["success", "failure"]'), github.event.workflow_run.conclusion) + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + actions: read # download the link-check run's artifact + outputs: + found: ${{ steps.links.outputs.found }} + links: ${{ steps.links.outputs.links }} + more: ${{ steps.links.outputs.more }} + steps: + # A run the property gate skipped uploads no artifact; that is not an + # error, so the next step decides. + - name: Download the dead links + id: download + continue-on-error: true + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: link-check-online-dead-links + path: ${{ runner.temp }}/dead-links + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - name: Check the dead links + id: links + env: + DOWNLOAD: ${{ steps.download.outcome }} + DEAD_LINKS: ${{ runner.temp }}/dead-links/dead-links.json + RUN_CONCLUSION: ${{ github.event.workflow_run.conclusion }} + MAX_LINKS: 50 + run: | + set -euo pipefail + if [[ "$DOWNLOAD" != success || ! -s "$DEAD_LINKS" ]]; then + if [[ "$RUN_CONCLUSION" == failure ]]; then + echo "::warning::the red link-check run left no dead-links artifact: nothing to file" + else + echo "the link-check run left no dead-links artifact: nothing to file" + fi + exit 0 + fi + # Each URL a plain http(s) URL, each path free of backticks and line + # breaks, so both sit safely in a code span; anything else is dropped. + # The server's text is never kept: it is written outside the org and + # intake reads the issue body. Each link keeps 5 locations and a count + # of the rest, and URLs and paths are length-capped, so 50 links stay + # well under the 128 KiB a single environment variable can hold when + # the file job reads them as LINKS. + if ! valid="$(jq -c ' + if type != "array" then error("not an array") else . end + | map(select((.url | type == "string" and length <= 1000 and test("^https?://[^\\s`<>]+$")) + and (.locations | type == "array"))) + | map({url, + status: (if (.status | type) == "number" then .status else null end), + all: [.locations[] + | select(type == "object" and (.path | type == "string" and test("^[^`\\r\\n]{1,200}$"))) + | {path, line: (if (.line | type) == "number" then .line else null end)}]}) + | map(select(.all | length > 0) + | {url, status, locations: .all[:5], more_locations: ((.all | length) - 5 | if . < 0 then 0 else . end)})' "$DEAD_LINKS")"; then + echo "::error::the dead-links artifact is not a JSON array" + exit 1 + fi + total="$(jq 'length' "$DEAD_LINKS")" + count="$(jq 'length' <<<"$valid")" + if ((count < total)); then + echo "::warning::dropped $((total - count)) of $total malformed dead-link entries" + fi + # Only an artifact that is itself empty means no dead links. One whose + # every entry failed the shape check (a malformed artifact or a format + # change upstream) hands on nothing, so the rollup is never closed as + # fixed on bad data. + if ((total > 0 && count == 0)); then + echo "::warning::every dead-link entry failed the shape check: the rollup is left as it is" + exit 0 + fi + links="$(jq -c --argjson max "$MAX_LINKS" '.[:$max]' <<<"$valid")" + more=$((count > MAX_LINKS ? count - MAX_LINKS : 0)) + delim="EOF_$(openssl rand -hex 16)" + { + echo "found=true" + echo "more=$more" + echo "links<<$delim" + echo "$links" + echo "$delim" + } >>"$GITHUB_OUTPUT" + echo "handed $((count - more)) dead links to the file job ($more more over the cap)" + + file: + name: Update the dead-link rollup issue + needs: [read] + if: needs.read.outputs.found == 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + issues: write # read, open, rewrite, comment on, close and reopen the rollup + # Two link-check runs finishing close together must not both find no + # rollup and open two. + concurrency: + group: maintenance-file-dead-links + cancel-in-progress: false + steps: + - name: Rewrite the rollup issue + env: + GH_TOKEN: ${{ github.token }} + LINKS: ${{ needs.read.outputs.links }} + MORE: ${{ needs.read.outputs.more }} + RUN_URL: ${{ github.event.workflow_run.html_url }} + MARKER: + DEMOTION_PATH: https://github.com/melodic-software/claude-code-plugins/blob/main/plugins/attribution/skills/audit/reference/dispositions.md#the-demotion-path-when-a-pointer-later-dies + run: | + set -euo pipefail + [[ "$MORE" =~ ^[0-9]+$ ]] || MORE=0 + # The rollups github-actions[bot] filed: an open one wins, else the + # newest. A failed read writes nothing, so it never opens a second. + # shellcheck disable=SC2016 # jq string interpolation, not shell + if ! listing="$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues?state=all&creator=github-actions%5Bbot%5D&per_page=100" \ + --jq '.[] | select(.pull_request == null) + | select((.body // "") | sub("^\\s+"; "") | split("\n")[0] | rtrimstr("\r") == env.MARKER) + | "\(.number) \(.state) \(.state_reason // "none")"')"; then + echo "::error::could not read the dead-link rollup issues: nothing written" + exit 1 + fi + number='' state='' reason='' + while read -r n s r; do + [[ "$n" =~ ^[0-9]+$ ]] || continue + if [[ -z "$number" || ( "$s" == open && "$state" != open ) || ( "$state" != open && "$s" != open && n -gt number ) ]]; then + number="$n" state="$s" reason="$r" + fi + done <<<"$listing" + count="$(jq 'length' <<<"$LINKS")" + + summary() { printf '# Dead-link rollup\n\n%s\n' "$1" >>"$GITHUB_STEP_SUMMARY"; echo "$1"; } + if ((count == 0)); then + if [[ "$state" != open ]]; then + summary "No dead links, and no open rollup issue: nothing to do." + exit 0 + fi + gh api -X POST "repos/$GITHUB_REPOSITORY/issues/$number/comments" \ + -f body="The link check reported no dead links in $RUN_URL: closing." >/dev/null + gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/$number" -f state=closed -f state_reason=completed >/dev/null + summary "No dead links: closed the rollup #$number as completed." + exit 0 + fi + if [[ "$state" == closed && "$reason" == not_planned ]]; then + summary "$count dead links, but the rollup #$number was closed as not planned: left alone. Reopen it to resume the list." + exit 0 + fi + + # The body with the first $1 links, the rest counted in the +N more + # line. Each item shows at most 5 locations. + render() { + jq -r --arg marker "$MARKER" --arg run "$RUN_URL" --arg demote "$DEMOTION_PATH" \ + --argjson keep "$1" --argjson more "$((MORE + count - $1))" ' + "\($marker)\n\n" + + "The weekly online link check found these links dead in \($run). Each run rewrites this list.\n\n" + + ([.[:$keep][] | "- [ ] `\(.url)`, status \(.status // "none"): " + + ([.locations[:5][] | "`\(.path)`" + (if .line then " line \(.line)" else "" end)] | join(", ")) + + (((.more_locations // 0) + ((.locations | length) - 5 | if . < 0 then 0 else . end)) as $k + | if $k > 0 then ", +\($k) more locations" else "" end)] + | join("\n")) + + (if $more > 0 then "\n- +\($more) more, see the run." else "" end) + + "\n\nThe status is the code alone: the text the server sent is left out, since it is written " + + "outside the org and intake reads this body.\n\n" + + "Repair each pointer by [the demotion path when a pointer later dies](\($demote)): " + + "point at a live source that holds the same fact, or demote the pointer to a stamped record " + + "or an archived-snapshot citation; never re-expand it into a copy. A page that is alive but " + + "refuses the checker gets an exact-URL `.lycheeignore` entry with a dated reason.\n\n" + + "Close this only once the link check no longer reports these URLs: removed, replaced, or in " + + "`.lycheeignore`. A run that reports none closes it. A dead URL kept in a link, the marked " + + "pointer of a demoted record included, is found again and reopens this issue on the next run.\n" + ' <<<"$LINKS" + } + # An issue body holds at most 65,536 characters; past 60,000, links + # move from the end of the list into the +N more line. + keep="$count" + body="$(render "$keep")" + while ((${#body} > 60000 && keep > 0)); do + keep=$((keep - 1)) + body="$(render "$keep")" + done + if [[ -z "$number" ]]; then + gh api -X POST "repos/$GITHUB_REPOSITORY/issues" -f 'labels[]=needs-triage' \ + -f title="Dead links found by the online link check" -f body="$body" >/dev/null + summary "Opened the rollup issue with $count dead links." + elif [[ "$state" == open ]]; then + gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/$number" -f body="$body" >/dev/null + summary "Rewrote the rollup #$number with $count dead links." + else + gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/$number" -f state=open -f body="$body" >/dev/null + summary "Reopened the rollup #$number with $count dead links." + fi diff --git a/scripts/maintenance-file-dead-links.test.sh b/scripts/maintenance-file-dead-links.test.sh new file mode 100755 index 00000000000..a46b201f467 --- /dev/null +++ b/scripts/maintenance-file-dead-links.test.sh @@ -0,0 +1,329 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 # jq programs and markdown code spans are literal +# Runs the two step bodies of .github/workflows/maintenance-file-dead-links.yml +# under the shell Actions gives a step with no `shell:` (bash -e): `Check the +# dead links` against artifact fixtures, and `Rewrite the rollup issue` +# against a stub `gh` that applies the step's own --jq filter to a fixture +# issue list and logs every write. Also checks the workflow's shape: the +# trigger names the link-check workflow, and each job holds one scope. +# test-scope: .github/workflows/maintenance-file-dead-links.yml +set -uo pipefail +unset GIT_DIR GIT_WORK_TREE GIT_CONFIG + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +WORKFLOW="$ROOT/.github/workflows/maintenance-file-dead-links.yml" +CALLER="$ROOT/.github/workflows/maintenance-check-links-online-claude-code-plugins.yml" + +# shellcheck source=lib/test-harness.sh +. "$ROOT/scripts/lib/test-harness.sh" + +TMP_ROOT="$(mktemp -d)" +trap 'rm -rf "$TMP_ROOT"' EXIT +export TMPDIR="$TMP_ROOT" + +# The `run: |` body of the step named $1, de-indented. +step_body() { + awk -v name=" - name: $1" '$0 == name { on = 1; next } + on && /^ - / { exit } + on { print }' "$WORKFLOW" | + awk '/^ run: \|$/ { on = 1; next } + on && /^ / { print substr($0, 11); next } + on && /^[[:blank:]]*$/ { print ""; next } + on { exit }' +} +step_body 'Check the dead links' >"$TMP_ROOT/read.sh" +step_body 'Rewrite the rollup issue' >"$TMP_ROOT/file.sh" +for body in read file; do + if [[ ! -s "$TMP_ROOT/$body.sh" ]]; then + fail "no $body step with a run: | body in $WORKFLOW" + test_harness::report + exit 1 + fi +done +# The rollup marker as the workflow sets it. +MARKER="$(sed -n 's/^ MARKER: //p' "$WORKFLOW")" + +# --- Workflow shape ----------------------------------------------------------- +job() { awk -v j=" $1:" '$0 == j { on = 1; next } on && /^ [a-z-]+:$/ { exit } on' "$WORKFLOW"; } +scopes() { grep -oE '^ +[a-z-]+: (read|write)( |$)' | sed 's/^ *//; s/ *$//' | sort | paste -sd ' '; } +caller_name="$(sed -n 's/^name: //p' "$CALLER")" +triggers="$(awk '/^ workflow_run:/ { on = 1; next } on && /^ [a-z]/ { exit } on' "$WORKFLOW")" +if [[ -n "$caller_name" && "$triggers" == *"workflows: [$caller_name]"* && "$triggers" == *"types: [completed]"* ]]; then + ok "workflow_run fires on completed $caller_name runs" +else + fail "workflow_run does not name the link-check workflow '$caller_name': '$triggers'" +fi +if grep -q '^permissions: {}$' "$WORKFLOW"; then + ok "the workflow grants nothing by default" +else + fail "the workflow-level permissions are not {}" +fi +if [[ "$(job read | scopes)" == 'actions: read' ]]; then + ok "the read job holds actions: read alone" +else + fail "the read job's permissions are not exactly actions: read: '$(job read | scopes)'" +fi +if [[ "$(job file | scopes)" == 'issues: write' ]] && ! grep -qE 'uses:|actions/checkout' <<<"$(job file)"; then + ok "the file job holds issues: write alone and checks out nothing" +else + fail "the file job's permissions are not exactly issues: write, or it uses an action" +fi +if grep -q 'name: link-check-online-dead-links$' <<<"$(job read)"; then + ok "the read job downloads the ci-workflows link-check-online-dead-links artifact" +else + fail "the read job does not download link-check-online-dead-links" +fi +if [[ "$MARKER" == '' ]]; then + ok "the rollup marker is the fixed link-check-dead-pointer rollup comment" +else + fail "the rollup marker is '$MARKER'" +fi + +# --- Check the dead links ----------------------------------------------------- +run_read() { # [MAX_LINKS] + local out="$TMP_ROOT/read-output" fixture="$TMP_ROOT/dead-links.json" + : >"$out" + rm -f "$fixture" + [[ -n "$3" ]] && printf '%s' "$3" >"$fixture" + READ_LOG="$(env -i PATH="$PATH" DOWNLOAD="$1" RUN_CONCLUSION="$2" DEAD_LINKS="$fixture" \ + MAX_LINKS="${4:-50}" GITHUB_OUTPUT="$out" bash -e "$TMP_ROOT/read.sh" 2>&1)" + READ_STATUS=$? + READ_OUT="$(cat "$out")" + READ_LINKS="$(awk '/^links<"$TMP_ROOT/bin/gh" <<'STUB' +#!/usr/bin/env bash +# Listing: apply the step's --jq filter to the fixture issues. Writes: log them. +if [[ "${GH_LIST_STATUS:-0}" != 0 && " $* " == *" --paginate "* ]]; then + exit "$GH_LIST_STATUS" +fi +args=("$@") filter='' method=GET path='' +for ((i = 0; i < ${#args[@]}; i++)); do + case "${args[i]}" in + --jq) filter="${args[i + 1]}" ;; + -X) method="${args[i + 1]}" ;; + repos/*) path="${args[i]}" ;; + esac +done +if [[ "$method" == GET ]]; then + jq -r "$filter" "$GH_ISSUES" + exit +fi +{ + printf '%s %s\n' "$method" "$path" + for ((i = 0; i < ${#args[@]}; i++)); do + [[ "${args[i]}" == -f ]] && printf 'F %s\n' "${args[i + 1]%%$'\n'*}" + done +} >>"$GH_LOG" +for ((i = 0; i < ${#args[@]}; i++)); do + [[ "${args[i]}" == -f && "${args[i + 1]}" == body=* ]] && printf '%s\n' "${args[i + 1]#body=}" >>"$GH_BODY" +done +exit "${GH_WRITE_STATUS:-0}" +STUB +chmod +x "$TMP_ROOT/bin/gh" + +issues() { jq -n --arg m "$MARKER" "$1" >"$TMP_ROOT/issues.json"; } +run_file() { # [VAR=value ...] + local links="$1" + shift + : >"$TMP_ROOT/gh.log" + : >"$TMP_ROOT/body" + : >"$TMP_ROOT/summary" + FILE_LOG="$(env -i PATH="$TMP_ROOT/bin:$PATH" GH_ISSUES="$TMP_ROOT/issues.json" GH_LOG="$TMP_ROOT/gh.log" \ + GH_BODY="$TMP_ROOT/body" GITHUB_REPOSITORY=o/r GITHUB_STEP_SUMMARY="$TMP_ROOT/summary" \ + LINKS="$links" MORE=0 RUN_URL=https://github.com/o/r/actions/runs/1 MARKER="$MARKER" \ + DEMOTION_PATH=https://example.com/dispositions "$@" bash -e "$TMP_ROOT/file.sh" 2>&1)" + FILE_STATUS=$? + GH_WRITES="$(cat "$TMP_ROOT/gh.log")" + BODY="$(cat "$TMP_ROOT/body")" + SUMMARY="$(cat "$TMP_ROOT/summary")" +} +two='[{"url":"https://example.com/a","status":404,"locations":[{"path":"docs/a.md","line":7},{"path":"docs/b.md","line":null}]},{"url":"https://example.com/b","status":null,"locations":[{"path":"README.md","line":2}]}]' + +# No rollup yet: a marker on a later line, or on a pull request, is not one. +issues '[{number: 4, state: "open", state_reason: null, body: ("intro\n" + $m)}, + {number: 5, state: "open", state_reason: null, body: $m, pull_request: {url: "x"}}]' +run_file "$two" +if ((FILE_STATUS == 0)) && [[ "$GH_WRITES" == "POST repos/o/r/issues +F labels[]=needs-triage +F title=Dead links found by the online link check +F body=$MARKER" ]]; then + ok "with no rollup, one issue is opened, labeled needs-triage; a misplaced marker is no rollup" +else + fail "no rollup did not open one issue: status $FILE_STATUS, writes: +$GH_WRITES +log: $FILE_LOG" +fi +# shellcheck disable=SC2016 # literal markdown code spans +if [[ "$BODY" == *'- [ ] `https://example.com/a`, status 404: `docs/a.md` line 7, `docs/b.md`'* && + "$BODY" == *'- [ ] `https://example.com/b`, status none: `README.md` line 2'* && + "$BODY" == *"found these links dead in https://github.com/o/r/actions/runs/1"* && + "$BODY" == *"(https://example.com/dispositions)"* && + "$BODY" == *"Close this only once the link check no longer reports these URLs"* && + "$BODY" != *"more, see the run"* ]]; then + ok "the body lists each link as a task with its status and locations, the run, the demotion path and when to close" +else + fail "the rollup body is missing a part: $BODY" +fi + +run_file "$two" MORE=7 +if [[ "$BODY" == *$'\n- +7 more, see the run.\n'* ]]; then + ok "links over the cap get a +N more line" +else + fail "no overflow line: $BODY" +fi + +# 50 long URLs with 20 locations each on long paths: the read step keeps 5 +# locations a link and stays under the 128 KiB a single environment variable +# holds, and the file step keeps the body under the 65,536-character limit. +big="$(jq -cn '[range(53) as $i | {url: ("https://example.com/" + ("u" * 950) + "/\($i)"), status: 404, + locations: [range(20) as $j | {path: ("docs/" + ("p" * 185) + "/\($j).md"), line: $j}]}]')" +run_read success failure "$big" +if ((READ_STATUS == 0)) && [[ "$(jq -c '[length, (map(.locations | length) | unique), (map(.more_locations) | unique)]' <<<"$READ_LINKS")" == '[50,[5],[15]]' ]] && + [[ "$READ_OUT" == *$'\nmore=3\n'* ]] && ((${#READ_LINKS} < 131072)); then + ok "the read step keeps 5 locations a link, counts the rest, and hands on under 128 KiB (${#READ_LINKS} bytes)" +else + fail "the read step did not cap locations or size: status $READ_STATUS, ${#READ_LINKS} bytes, $READ_LOG" +fi +run_file "$READ_LINKS" MORE=3 +shown="$(grep -c '^- \[ \] ' <<<"$BODY")" +over="$(sed -n 's/^- +\([0-9]*\) more, see the run\.$/\1/p' <<<"$BODY")" +if ((FILE_STATUS == 0 && ${#BODY} <= 60001 && shown > 0 && shown < 50)) && [[ "$over" == "$((50 - shown + 3))" ]] && + [[ "$BODY" == *", +15 more locations"* ]] && ! grep -q '/5\.md' <<<"$BODY"; then + ok "a body over 60,000 characters moves links into the +N more line; each item shows 5 locations and counts the rest" +else + fail "the big body was not capped: status $FILE_STATUS, ${#BODY} characters, $shown shown, over '$over'" +fi + +# An open rollup wins over an older closed one. +issues '[{number: 9, state: "closed", state_reason: "completed", body: $m}, + {number: 8, state: "open", state_reason: null, body: ("\n" + $m + "\n\nold")}]' +run_file "$two" +if ((FILE_STATUS == 0)) && [[ "$GH_WRITES" == "PATCH repos/o/r/issues/8 +F body=$MARKER" && "$SUMMARY" == *"Rewrote the rollup #8 with 2 dead links."* ]]; then + ok "an open rollup has its list rewritten" +else + fail "the open rollup was not rewritten: $FILE_STATUS $GH_WRITES $FILE_LOG" +fi + +run_file '[]' +if ((FILE_STATUS == 0)) && [[ "$GH_WRITES" == "POST repos/o/r/issues/8/comments +F body=The link check reported no dead links in https://github.com/o/r/actions/runs/1: closing. +PATCH repos/o/r/issues/8 +F state=closed +F state_reason=completed" ]]; then + ok "with no dead links, the open rollup gets a comment and closes as completed" +else + fail "the empty run did not close the rollup: $FILE_STATUS $GH_WRITES $FILE_LOG" +fi + +issues '[{number: 3, state: "closed", state_reason: "completed", body: $m}, + {number: 6, state: "closed", state_reason: "completed", body: $m}]' +run_file '[]' +if ((FILE_STATUS == 0)) && [[ -z "$GH_WRITES" ]]; then + ok "with no dead links and no open rollup, nothing is written" +else + fail "the empty run wrote with no open rollup: $GH_WRITES" +fi +run_file "$two" +if ((FILE_STATUS == 0)) && [[ "$GH_WRITES" == "PATCH repos/o/r/issues/6 +F state=open +F body=$MARKER" && "$SUMMARY" == *"Reopened the rollup #6"* ]]; then + ok "a rollup closed as completed, the newest, is reopened with the new list" +else + fail "the completed rollup was not reopened: $FILE_STATUS $GH_WRITES $FILE_LOG" +fi + +issues '[{number: 6, state: "closed", state_reason: "not_planned", body: $m}]' +run_file "$two" +if ((FILE_STATUS == 0)) && [[ -z "$GH_WRITES" && "$SUMMARY" == *"the rollup #6 was closed as not planned: left alone"* ]]; then + ok "a rollup closed as not planned is left alone, and the summary says so" +else + fail "the not-planned rollup was touched: $FILE_STATUS $GH_WRITES $SUMMARY" +fi + +run_file "$two" GH_LIST_STATUS=1 +if ((FILE_STATUS == 1)) && [[ -z "$GH_WRITES" && "$FILE_LOG" == *"::error::could not read the dead-link rollup issues"* ]]; then + ok "a failed rollup read writes nothing and fails" +else + fail "a failed rollup read did not stop the filer: $FILE_STATUS $GH_WRITES $FILE_LOG" +fi + +issues '[]' +run_file "$two" GH_WRITE_STATUS=1 +if ((FILE_STATUS != 0)); then + ok "a failed write fails the job" +else + fail "a failed write did not fail the job: $FILE_LOG" +fi + +test_harness::report