From f0ebe93ecdf84afc39bc37f6dcc5df05e2a412a1 Mon Sep 17 00:00:00 2001 From: stxkxs <139715017+stxkxs@users.noreply.github.com> Date: Tue, 18 Aug 2026 23:15:50 -0700 Subject: [PATCH 1/2] Add markdownlint and link-checking CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repository had no CI of any kind. It is a public marketing surface carrying four external links and time-sensitive factual claims, with nothing verifying either — the gap behind the testing grade in the 2026-08-18 sweep. ──────────────────────── The workflow ──────────────────────── `.github/workflows/checks.yml` runs two independent jobs: - **markdownlint** (markdownlint-cli2) — structure and style across every tracked markdown file. - **link check** (lychee) — every external and relative link actually resolves. Accepts 200/206/301/302/403, retries three times, and times out at 20s so a slow upstream is not read as a broken link. Triggers on pull_request and on push to main, plus a weekly cron and manual dispatch. The schedule is the point of the job as much as the PR trigger: external link rot happens with no commit attached to it, so a check that only runs on change would never catch a dead upstream until someone happened to edit the file. `permissions: contents: read` — the workflow needs nothing else. Concurrency cancels superseded runs per ref. Actions are pinned to full commit SHAs with the version in a trailing comment, resolved from the registry at authoring time rather than written from memory: actions/checkout v7.0.1, markdownlint-cli2-action v24.2.0, lychee-action v2.9.0. A mutable tag on a third-party action is a supply chain hole; a SHA is not. ────────────────────── The lint config ────────────────────── `.markdownlint-cli2.yaml` relaxes three rules deliberately, each with the reason recorded inline: - MD013 at 100 columns, off for tables and code blocks. Prose is wrapped by hand; tables and links cannot always honour a limit. - MD033 allows only the block elements the org profile actually needs — div, img, sub, b, a, br, picture, source. Raw HTML is how a GitHub profile centres a brand lockup and how it serves a theme-aware image. - MD024 siblings_only, so separate files may repeat a heading. LICENSE is excluded from the glob; it is legal text, not prose to lint. Co-authored-by: stxkxsbot <275011021+stxkxsbot@users.noreply.github.com> --- .github/workflows/checks.yml | 43 ++++++++++++++++++++++++++++++++++++ .markdownlint-cli2.yaml | 20 +++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 .github/workflows/checks.yml create mode 100644 .markdownlint-cli2.yaml diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..9a6a4a3 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,43 @@ +name: checks + +on: + pull_request: + push: + branches: [main] + schedule: + # Weekly, so external link rot surfaces on its own rather than at the next edit. + - cron: "17 9 * * 1" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: checks-${{ github.ref }} + cancel-in-progress: true + +jobs: + markdown: + name: markdownlint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: DavidAnson/markdownlint-cli2-action@21c1be1b93ad9ed58fa840aacc3f279cde2a72ff # v24.2.0 + with: + globs: "**/*.md" + + links: + name: link check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0 + with: + args: >- + --no-progress + --include-verbatim + --max-retries 3 + --timeout 20 + --accept 200,206,301,302,403 + . + fail: true diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml new file mode 100644 index 0000000..b49742c --- /dev/null +++ b/.markdownlint-cli2.yaml @@ -0,0 +1,20 @@ +# The profile README is a rendered GitHub page, not a document tree, so a few +# rules that assume prose-document structure are relaxed deliberately. +config: + # Line length: prose is wrapped by hand at ~80. Tables, links, and the inline + # HTML the org profile needs cannot always honour that. + MD013: + line_length: 100 + tables: false + code_blocks: false + # GitHub renders raw HTML in profile READMEs and it is the only way to centre + # the brand lockup. Allowed, but kept to the block elements actually needed. + MD033: + allowed_elements: [div, img, sub, b, a, br, picture, source] + # Duplicate headings are fine across separate files in this repo. + MD024: + siblings_only: true + +globs: + - "**/*.md" + - "!LICENSE" From 19f5fc46fccde8d9fdf625c9655dec1ec6f47842 Mon Sep 17 00:00:00 2001 From: stxkxs <139715017+stxkxs@users.noreply.github.com> Date: Tue, 18 Aug 2026 23:18:56 -0700 Subject: [PATCH 2/2] ci: assert the h1 that the MD041 exception depends on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit profile/README.md carries a scoped MD041 disable because the brand lockup has to precede the heading. The comment there claims the property MD041 protects is still satisfied — a top-level heading naming the document — and that claim is true today. But a suppression outlives the reason it was granted. Delete that h1 and MD041 is off on the file, so nothing fails and the accessibility defect returns silently. The disable would then be asserting something false. Adds a two-line assertion to the markdown job: profile/README.md must contain exactly one top-level heading. This pins the property rather than trusting the comment, and converts the suppression from a promise into a checked invariant. Verified: the fixed README returns 1 and passes; main's current README returns 0 and fails, which is the correct behaviour until the profile fix lands ahead of this workflow. Co-authored-by: stxkxsbot <275011021+stxkxsbot@users.noreply.github.com> --- .github/workflows/checks.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 9a6a4a3..9bd09bd 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -26,6 +26,20 @@ jobs: with: globs: "**/*.md" + - name: Pin the MD041 exception + # profile/README.md carries a scoped MD041 disable, because the brand + # lockup has to precede the h1. That suppression is only honest while the + # h1 it points at actually exists — otherwise deleting the heading would + # silently reintroduce the accessibility defect with no check failing. + # Assert the property the rule was protecting instead of trusting it. + run: | + count=$(grep -c '^# ' profile/README.md || true) + if [ "$count" -ne 1 ]; then + echo "::error file=profile/README.md::expected exactly 1 top-level heading, found $count" + exit 1 + fi + echo "profile/README.md has exactly one h1" + links: name: link check runs-on: ubuntu-latest