Skip to content

feat(maintenance-check-links-online): upload dead links as a short-lived artifact - #993

Merged
kyle-sexton merged 2 commits into
mainfrom
link-check-dead-links-artifact
Nov 5, 2026
Merged

kyle-sexton merged 2 commits into
mainfrom
link-check-dead-links-artifact

Conversation

@kyle-sexton

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

Copy link
Copy Markdown
Member

Refs: melodic-software/claude-code-plugins#9759

Summary

The online link check reported dead links only in its job summary, so nothing downstream could act on them. Each run that checked links now uploads its broken links as a small JSON artifact and names it in two new workflow outputs. This is step 3 of claude-code-plugins#9759, owner decision Q-9759-1 (a), recorded here. A claude-code-plugins pull request adds the local workflow that files them; it needs this one merged first.

Fix

.github/workflows/maintenance-check-links-online.yml:

  • New step Extract the dead links, after Write the report, under steps.gate.outputs.run == 'true' && !cancelled(), so it still runs on the red runs that have broken links. It reads lychee's error_map only (timeouts and accepted 429/5xx are left out) and writes an array of {url, status, detail, locations: [{path, line, column}]}, one entry per URL, sorted by URL. A run with no broken links writes [], so a consumer can tell "clean" from "did not run". A missing or non-JSON report, which the report step already fails, writes nothing.
  • New step Upload the dead links: actions/upload-artifact (same pinned SHA as pr-review-cli.yml), artifact link-check-online-dead-links, retention-days: 3.
  • New workflow_call.outputs: dead-links-artifact (the artifact name, empty when nothing uploaded) and dead-links-count.
  • No new input, so no runner-policy allowedInputs change. Permissions unchanged (contents: read): upload-artifact uses the runner's runtime token, not GITHUB_TOKEN.

Every caller now stores this artifact, private repositories included. The payload is one small JSON file and it expires after three days.

README.md says what the outputs and artifact are.

Verification

  • node --test .github/scripts/maintenance-check-links-online.test.cjs: 19 pass, 0 fail. New tests cover the outputs and the unchanged input list, the upload's pin, name and retention, grouping one URL's locations across files, the empty list, and no artifact for a missing or non-JSON report.
  • node --test ".github/scripts/*.test.cjs": 912 pass, 0 fail.
  • actionlint on the workflow: exit 0.
  • zizmor --offline on the workflow: no findings.
  • biome ci on the test file: clean.

Related

🤖 Generated with Claude Code

…ved artifact

Each run that checked links uploads its broken links (error_map only, one
entry per URL with every location) as the link-check-online-dead-links JSON
artifact, kept three days, and names it in the new dead-links-artifact and
dead-links-count workflow outputs, so a caller can route them. No new input
and no permission change: upload-artifact uses the runtime token.

Refs: melodic-software/claude-code-plugins#9759

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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:34
@kyle-sexton
kyle-sexton added this pull request to the merge queue Nov 5, 2026
@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:05.136872Z 3b29415 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.

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

claude-security-review reviewed 3b294159d5309853382bef62ca1f63df61095ffc: 0 finding(s), 0 as inline comments.

Security review: no findings

I read the full diff in .claude-lane/pr.diff (3 files: maintenance-check-links-online.yml, its test file, and README.md). I found no security issues.

critical-findings: 0

What I checked:

  • Trust controls: none are touched. The diff changes no trusted-actor list, token scope, kill switch, merge hold, or .github/actions/* file. The job keeps permissions: contents: read and the workflow keeps permissions: {}. It still declares no secrets.
  • New action: actions/upload-artifact is pinned to a full 40-character SHA (cf430e03…, v7.0.2). The test also asserts the pin format.
  • Shell injection in the dead step:
    • REPORT, DEAD_LINKS and ARTIFACT come from runner.temp or a literal, and they reach the script as environment variables, not through ${{ }} interpolation in the script body.
    • The jq filter is a fixed program. Report content is only data to it.
    • Newlines in detail are collapsed with gsub, but the fields written to $GITHUB_OUTPUT (the fixed artifact name and the numeric jq length count) never carry report text. Remote-host-controlled text therefore cannot inject extra output keys.
  • Outputs: the workflow-level and job-level outputs expose only the fixed artifact name and an integer count. No secret or untrusted text flows into the output channel.
  • Artifact contents: the artifact holds URLs and locations already present in the repository checkout, plus lychee's status text. It does not hold the GITHUB_TOKEN. Retention is 3 days, and if-no-files-found: error fails a missing file loudly.
  • Downstream consumers: the README and workflow comment describe a caller or a workflow_run workflow that routes the dead links, but no such consumer is in this diff. The artifact's detail field holds text a remote server returned, so a future consumer should treat it as untrusted. I raised no finding because the diff has no reachable path to a part 2 impact.
  • Instruction-surface deletions and plugin manifests: neither applies. The diff touches no CLAUDE.md, AGENTS.md, rules file, skill, agent body, or plugin manifest.

No hold is needed.

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

claude-review reviewed 3b294159d5309853382bef62ca1f63df61095ffc: 0 finding(s), 0 as inline comments.

No findings. I reviewed the 3-file diff (maintenance-check-links-online.yml, its test file, and README.md) against the PR context and .claude/rules/pr-body-contract.md. I read the workflow and test around the changed lines. I did not run the tests or the jq filter myself; the PR body's test, actionlint and zizmor results are author-claimed and unverified by me.

What I checked:

  • Step conditions. Extract the dead links runs under steps.gate.outputs.run == 'true' && !cancelled(), the same condition as the report step, so it still runs after the report step fails on a red run. The upload step is gated on steps.dead.outputs.artifact != '' && !cancelled(). A missing or non-JSON report writes no artifact output and skips the upload, matching the PR description.
  • jq extraction. The filter reads error_map only, the same shape the report step already iterates. It groups by URL, de-duplicates locations, and gives [] on a clean run. The // fallbacks bind before the pipe into gsub. An absent error_map and null status.code or span values are handled.
  • Job and workflow outputs. They are non-empty only when steps.upload.outcome == 'success', so a failed upload cannot advertise an artifact that does not exist. The artifact's if-no-files-found: error covers the missing-file case.
  • Pin and permissions. The actions/upload-artifact SHA matches the one pr-review-cli.yml already uses (cf430e0…, v7.0.2). Job permissions stay contents: read, and no input is added, so the runner-policy allowedInputs is unaffected.
  • Tests. The new tests exercise the real step script through bash with a temporary GITHUB_OUTPUT. They cover the pin, name and retention, the output and input key lists, location grouping, the empty list, and the missing and non-JSON report cases. The destructuring [gate, checkout, lychee, report, dead, upload] matches the new step order.
  • Closing issue. The PR uses Refs:, not Closes, so there is no closing-issue acceptance criteria to check.
  • Manifests, paved paths, settled threads, prior findings. No plugin manifests changed, and no settled-threads or prior-findings files were supplied.

Two observations, both below the bar for a finding:

  • The artifact name link-check-online-dead-links is fixed. A caller that invokes this reusable workflow twice in one workflow run would hit upload-artifact's duplicate-name error. I saw no sign of such a caller, and the PR names a single consumer.
  • The header comment says the artifact holds "the ones that fail the run". Timeouts can also fail the run when the config doesn't set accept_timeouts, and they are deliberately left out, per the PR description. This is a wording imprecision, not a defect.

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