Skip to content

feat(ci): file one issue per dead link the online link check finds - #13549

Merged
kyle-sexton merged 7 commits into
mainfrom
file-dead-links-9759
Nov 5, 2026
Merged

kyle-sexton merged 7 commits into
mainfrom
file-dead-links-9759

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Refs: #9759

Merge melodic-software/ci-workflows#993 first. It makes the reusable link check upload the artifact this workflow reads. Until it merges, the read job finds no artifact and writes nothing; on a red run it also logs a warning.

Summary

Step 3 of #9759: the dead links the weekly online link check finds now go to one rollup issue for the repository, which points at the attribution audit's demotion path when a pointer later dies.

This follows two owner decisions:

  • Q-9759-1 (a), recorded on the issue: a ci-workflows link-check output plus a local filer.
  • "One rollup issue", recorded on this PR, in place of one issue per link.

Fix

New .github/workflows/maintenance-file-dead-links.yml. It runs on workflow_run when the maintenance-check-links-online-claude-code-plugins run completes, on main in this repository, and only if that run concluded success or failure.

That caller is sync-managed from melodic-software/standards, so this PR cannot add a job to it. A reusable workflow's outputs reach only its own caller, so this workflow reads the artifact instead.

read job, actions: read only.

  • Downloads link-check-online-dead-links from the triggering run.
  • Keeps an entry only if its URL is plain http(s) with no backtick, whitespace or angle bracket, and its paths have no backtick or line break.
  • Keeps only the URL, the status code and the locations. The server's text is dropped, because it is written outside the org and intake reads the issue body.
  • Keeps 5 locations a link plus a count of the rest, and caps URLs at 1,000 characters and paths at 200, so the handed-on list stays under the 128 KiB one environment variable holds.
  • Hands on at most 50 links, plus the count over that cap. An empty list is handed on only when the artifact itself is empty, so the rollup can close; an artifact whose every entry fails the shape check warns and hands on nothing, so bad data never closes the rollup.
  • If the run uploaded no artifact, it hands on nothing and the rollup stays as it is. That is quiet on a green run and a warning on a red one.

file job, issues: write only, no checkout. This is the same split as #13466's file-revert.

  • Finding the rollup: an issue opened by github-actions[bot] whose first body line is <!-- link-check-dead-pointer rollup -->. An open rollup wins over a closed one; otherwise the newest is used.
  • Dead links found:
    • With no rollup, it opens one titled "Dead links found by the online link check" and labeled needs-triage.
    • An open rollup has its list rewritten.
    • A rollup closed as completed is reopened with the new list.
    • A rollup closed as not planned is left alone, and the job summary says so.
  • No dead links: an open rollup gets a short comment linking the run and is closed as completed. With no open rollup, nothing is written.
  • Failures: if listing the existing rollups fails, it writes nothing and fails, so it never opens a second rollup. A failed write fails the job.
  • Why it adds the label itself: an issue the GITHUB_TOKEN opens raises no event, so intake-triage.yml never adds its needs-triage floor label on open (intake-triage.yml:20). The hourly triage sweep also leaves github-actions[bot] issues out (.github/scripts/maintenance-audit-lane-runs/triage-sweep.mjs:18-19).

The rollup body, in order:

  1. The marker.
  2. The run link.
  3. One - [ ] item per link: the URL and status code as `URL`, status 404: `path` line N, `path2`.
  4. A - +N more, see the run. line when links go over the cap, or when the body passes 60,000 characters and links move from the end of the list into that count (the issue limit is 65,536). Each item shows at most 5 locations and ends , +K more locations when there are more.
  5. A note that the server's text is left out.
  6. The demotion-path repair guidance.
  7. "Close this only once the link check no longer reports these URLs", with the note that a run reporting none closes it and that a dead URL left in a link reopens it.

api-budget-coverage.test.mjs gets an ALLOWED entry for the file job. With no checkout, the job cannot load log-api-budget, the same situation as file-revert.

The trigger names the link check by its name:, which the standards component owns. A rename there would disconnect this workflow; the test reads the caller's name: and fails on a mismatch.

No changelog fragment: no plugin file changes.

Verification

  • bash scripts/maintenance-file-dead-links.test.sh: exit 0, PASS=27 FAIL=0. New cases: an all-malformed artifact hands on nothing; 53 links with 950-character URLs and 20 locations each stay under 128 KiB in the read step and under 60,000 characters in the body.
  • bash scripts/check-pipefail-grep-q.sh: exit 0.
    • Workflow shape: the trigger, permissions: {}, read = actions: read, file = issues: write with no action, and the fixed marker.
    • Read step, against artifact fixtures: dropped entries, the server's text removed, the cap with its over count, the empty list handed on, a missing artifact on green and red runs, a non-array artifact.
    • File step, against a stub gh that applies the step's own --jq filter:
      • create with needs-triage, where a misplaced marker or a pull request is not a rollup;
      • the body parts;
      • the +N more line;
      • update of the open rollup over an older closed one;
      • close as completed with a comment when empty, and no write when empty with no open rollup;
      • reopen of the newest completed rollup;
      • not-planned left alone, with a summary line;
      • failed read;
      • failed write.
  • actionlint .github/workflows/maintenance-file-dead-links.yml: exit 0.
  • zizmor --offline on it: exit 0, no findings (1 ignored: dangerous-triggers on workflow_run, commented).
  • node --test .github/actions/resolve-config/api-budget-coverage.test.mjs: exit 0, 5 pass.
  • Pre-push check-affected: passed.

Remaining

Related

🤖 Generated with Claude Code

maintenance-file-dead-links.yml runs on each completed run of the
sync-managed online link check. A read job holding actions: read alone
downloads the ci-workflows link-check-online-dead-links artifact and checks
its shape; a file job holding issues: write alone files, updates or reopens
one issue per URL, found by the hidden first-line marker
<!-- link-check-dead-pointer url-sha256=<hex> -->, and points each at the
attribution audit's demotion path for a dead pointer.

Refs: #9759

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kyle-sexton and others added 3 commits November 5, 2026 11:27
…e one

An issue the GITHUB_TOKEN opens raises no event, so intake never adds the
needs-triage floor label, and the hourly triage sweep leaves
github-actions[bot] issues out: the filer adds the label itself. The body
now says a close sticks only once the link check stops reporting the URL,
and puts the server's status detail in a code span.

Refs: #9759

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The status detail is written by the remote server, outside the org, and intake's model reads
these issue bodies, so the body keeps only the status code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as ready for review November 5, 2026 16:35
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-05T16:36:44.158486Z eabe87b Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@kyle-sexton kyle-sexton added the do-not-merge Hard merge gate: do not merge while applied. label Nov 5, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eabe87baa6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@kyle-sexton

Copy link
Copy Markdown
Member Author

Owner decision on the filing shape, 2026-11-05, in session, picked from an AskUserQuestion prompt. The question: one issue per dead link, which would add about 19 open issues over two weeks given the 11-03 run's 19 dead URLs; one rollup issue; or hold until zero. The owner's answer, verbatim:

One rollup issue (Recommended)

This PR is reworked to keep one open rollup issue per repository. Each dead link is a - [ ] item, every run updates the list, and the issue closes when the list is empty. do-not-merge stays until the rework lands.

The owner chose one rollup issue. The file job now finds the issue
github-actions[bot] filed whose first body line is
<!-- link-check-dead-pointer rollup --> and rewrites its task list of dead
links (URL, status code, locations) each run. With no dead links it closes
an open rollup as completed with a comment; a completed rollup is reopened
when links die again, one closed as not planned is left alone, and a new
one, labeled needs-triage, is opened only when none exists. The read job
hands on an empty list so the rollup can close, drops the server's text,
and counts the links over the 50-link cap for a "+N more" line. The
per-link marker, the 10-new-issue cap and the write spacing are gone.

Refs: #9759

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review reviewed 1cb530c05098e76b4ca552b9014dc7684d809ee4: read 2 of 2 changed file(s); 0 finding(s), 0 as inline comments.

Security review: PR #13549, incremental (3921082 → 1cb530c), dead-link rollup size caps

No security issues found. An earlier head was already reviewed, so this pass was blocking only: SUGGESTION findings would have been dropped and only CRITICAL or IMPORTANT ones posted. I read every hunk of both files on the incremental list:

  • .github/workflows/maintenance-file-dead-links.yml
  • scripts/maintenance-file-dead-links.test.sh

Neither file matches a trusted-path glob.

critical-findings: 0

What changed

  • Read step, all entries malformed: when every entry fails the shape check (total > 0 && count == 0), the step now exits early. It does not hand on an empty list, so the rollup is no longer closed on bad data.
  • Read step, size caps:
    • URLs are capped at 1,000 characters and paths at 200.
    • Each link keeps 5 locations; the rest become a numeric more_locations count.
  • File step: the body is built by a render() function. If it is over 60,000 characters, links move from the end of the list into the +N more line until it fits.

What I checked

  • Untrusted text reaching triage. The needs-triage body still holds only the following (L198-L218):

    • URLs and paths that passed the same validation regexes, now with tighter length limits;
    • numeric statuses and counts;
    • the run URL;
    • fixed prose.

    The new more_locations field is computed by jq from an array length in the read step, so it is always a number. The server's text is still dropped.

  • Injection. Every value reaching jq --argjson is a shell integer:

    • keep starts from count, which is jq 'length' over LINKS, and only counts down;
    • MORE + count - $1 is arithmetic over MORE, which is still checked against ^[0-9]+$ (L159), and those same integers.

    There is still no ${{ }} inside run:. GITHUB_OUTPUT still uses the random heredoc delimiter.

  • Loop bound. The while loop stops at keep > 0, so it always ends.

  • Permissions and trigger: unchanged. actions: read on read, issues: write on file, no checkout, no secrets, no lane App token.

  • Test script: only test changes. It swaps job … | grep pipes for here-strings and adds an all-malformed case and a large-input case.

Trust controls, kill switch, merge holds and plugin manifests: none touched. Nothing in this push spends credit or can bill.

Settled threads

Both lane findings from the earlier head are fixed in this push, as the owner's replies say:

  • the rollup closed on an all-malformed artifact;
  • the issue body could exceed 65,536 characters.

I confirmed both fixes in the diff. I raised nothing new on those lines.

Its jq programs and markdown code spans are literal single-quoted text.

Refs: #9759

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

claude-review reviewed 1cb530c05098e76b4ca552b9014dc7684d809ee4: read 2 of 2 changed file(s); 0 finding(s), 0 as inline comments.

Code review: PR #13549, incremental review of 39210825..1cb530c0 (blocking findings only)

This lane already reviewed an earlier head, so this pass reports blocking findings only, and it found none. I read every hunk of both files in the incremental diff:

  • .github/workflows/maintenance-file-dead-links.yml
  • scripts/maintenance-file-dead-links.test.sh

Both earlier findings are fixed

  1. Bad data closing the rollup. When every artifact entry fails the shape check, the read step now warns and exits 0 before it writes found=true (maintenance-file-dead-links.yml L119–122). The file job therefore doesn't run, and the open rollup is left alone. Only an artifact that is itself empty ([]) still hands on an empty list, so a clean run can still close the rollup. A new test feeds an all-malformed fixture and expects an empty READ_OUT and the warning (test L133–138).

  2. Rollup body over GitHub's size limit. The cap now works in two places:

    • Read step: each link keeps 5 locations plus a more_locations count (L102–106). URLs are capped at 1,000 characters and paths at 200, which keeps LINKS under the 128 KiB a single environment variable can hold.
    • File step: render() moves links from the end of the list into the +N more count until the body is 60,000 characters or less (L196–227). The overflow count MORE + count - keep is correct.

    The new test uses 53 links with 950-character URLs and 20 locations each on long paths. It covers the location cap, more=3, the size of the handed-on list, and the body cap with its +N more arithmetic. It passes LINKS through env -i, so it also exercises the environment-variable size in practice.

Other changes in this push

  • Test cleanup. Two job … | grep -q pipes became here-strings (test L66, L71). This avoids SIGPIPE under pipefail, and behaviour is otherwise unchanged.
  • Older list shape still renders. render() still works on a list without more_locations, through // 0 and the .locations[:5] re-cap.

Closing issue

pr-context.md has no ## Closing issue section. The PR refs #9759 and closes nothing, so there are no acceptance criteria to check. The PR body's description of the new caps and the all-malformed behaviour matches the code.

Verification

I had no shell and ran nothing. The PR body reports PASS=27 FAIL=0, plus actionlint, zizmor and node --test passing. Those results are the author's claim and I have not confirmed them.

No plugin manifests changed. Spend: one weekly run on a standard ubuntu-24.04 runner, up to 50 extra jq calls per run when the body is trimmed, and no Claude credit.

@kyle-sexton kyle-sexton removed the do-not-merge Hard merge gate: do not merge while applied. label Nov 5, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude-review: 2 inline finding(s) on 39210825fc3f3feaf3826bc45ee6085cafc21e86; the summary is in this lane's pull request comment.

claude-lane: claude-review

- The read job hands on an empty list only when the artifact itself is
  empty. An artifact whose every entry fails the shape check warns and
  hands on nothing, so a malformed artifact or a format change upstream
  never closes the rollup as fixed.
- Each link keeps 5 locations and a count of the rest, shown as "+K more
  locations", and URLs and paths are length-capped, so 50 links stay under
  the 128 KiB one environment variable holds.
- A rollup body over 60,000 characters moves links from the end into the
  "+N more, see the run" line, under the 65,536-character issue limit.
- The test's two piped grep -q checks read here-strings instead.

Refs: #9759

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant