Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/shell-escape-residue.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
name: Shell Escape Residue

# Why this is its own workflow rather than a step in `ci.yml` or `lint.yml`: both
# of those decide inside the job whether the change "needs a full run", with an
# exclusion list that skips every expensive step on a markdown-only or
# changeset-only change — and a markdown-only change is precisely the shape that
# can introduce this defect. A gate that cannot see the pull request shape most
# likely to trip it "rebuilds the hole it exists to close": the conclusion
# `docs-links.yml`, `control-bytes.yml`, `skills-paths.yml`,
# `changeset-presence.yml`, `pre-install-import-graph.yml` and
# `vi-mock-specifiers.yml` have each reached in their own headers. One gate, one
# home.
#
# Hence: no `paths` and no `paths-ignore` here, deliberately.
# `scripts/__tests__/check-shell-escape-residue.test.ts` fails if either is ever
# added, and fails too if a second workflow starts running the same script.
# Reporting on every pull request is also what makes the check requirable, and
# `scripts/dependabot-merge-gate.mjs` classifies it as a required context — an
# unclassified blocking check is one a Dependabot merge would be let past
# (objectui#6135), and the `merge_group` floor now DERIVES from that same list
# (objectui#6160).
#
# It needs no install and no build — a checkout plus one `node` call over ~200
# markdown documents and ~1300 fenced blocks. Keep it that way; the import graph
# is builtins plus repo-relative modules only, which `pre-install-import-graph.yml`
# enforces (objectui#6148).

on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]
# Merge queue (objectui#3523 — see `ci.yml`'s trigger block for the full note
# and the measurements behind it). A required check that does not report on a
# queue build stalls the queue until the ruleset's 60-minute timeout fails it,
# so an unfiltered gate that can become required subscribes here from the
# start. `types:` is named although `checks_requested` is currently the only
# activity type GitHub defines for `merge_group`.
merge_group:
types: [checks_requested]
workflow_dispatch:

concurrency:
group: shell-escape-residue-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
shell-escape-residue:
name: Shell Escape Residue Scan
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- name: Checkout code
uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22.x'

# objectui#5150: the `git commit -F -` example in AGENTS.md §9 shipped with
# its heredoc terminator wrapped in the single-quote-inside-single-quote
# shell escape. Copied verbatim it does not fail with a message — it HANGS,
# on a terminator that never matches, and a reader does not attribute a hung
# terminal to the document. objectui#5151 measured the full derived gate
# union against the replanted bytes: control-bytes, doc-links and both
# changeset gates all exited 0, none of them negligently — the residue is
# printable ASCII inside a code block and no scan surface reached it.
#
# ⛔ This gate checks an ENUMERATED LITERAL. It does NOT make fenced shell
# examples executable-by-construction, and nothing in this repository does;
# `bash -n` per block is objectui#5151's unbuilt "direction 1". The script
# header states the boundary and the test suite asserts it as a fact.
#
# GREEN AT REST — there is nothing to find on an ordinary day — so it prints
# its per-root census rather than a bare "OK", and it FAILS when a scan root
# does not resolve or the population collapses. A scan that silently finds
# nothing reads as coverage, which is this gate's own defect one level up.
- name: Check for machine-produced shell-escape residue in fenced examples
run: node scripts/check-shell-escape-residue.mjs
57 changes: 57 additions & 0 deletions content/docs/guide/ci-cd-pipeline.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,7 @@ one has its own section below.
| `doc-fence-languages.yml` | Doc Fence Language Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a TypeScript block sits under a fence the snippet gate does not read |
| `pre-install-import-graph.yml` | Pre-Install Import Graph Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a gate a workflow runs *before* `pnpm install` reaches a package anywhere in its import graph |
| `vi-mock-specifiers.yml` | Inert vi.mock Specifier Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a `vi.mock` / `vi.doMock` relative specifier resolves to no file, or the scan's population collapses |
| `shell-escape-residue.yml` | Shell Escape Residue Scan | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a fenced block in `AGENTS.md`, `CLAUDE.md`, `skills/**` or `content/docs/**` carries the enumerated machine-produced shell escape, or a scan root fails to resolve |
| `performance-budget.yml` | Bundle Analysis | Push / PR touching `packages/**`, `apps/console/**`, `pnpm-lock.yaml` | **Yes** — the console entry gzip budget |
| `live-e2e.yml` | Live E2E (informational) | PR to `main`, `develop` (code paths); nightly cron `30 6 * * *`; manual | No — informational lane, `continue-on-error` |
| `labeler.yml` | Auto Label PRs | PR `opened`, `synchronize`, `reopened` | No |
Expand DownExpand Up@@ -831,6 +832,62 @@ that the suite goes red. Run it locally with `pnpm check:vi-mock-specifiers`, or
`node scripts/check-vi-mock-specifiers.mjs --list` to see every call site the walk found. It needs no
install and no build.

## Shell Escape Residue (`shell-escape-residue.yml`)

**Triggers:** Push and PR to `main`/`develop`, merge-queue builds, plus manual dispatch — with **no
path filter at all**. The scan surface is markdown that any shape of pull request can touch, and a
markdown-only change is exactly the shape `ci.yml` and `lint.yml` skip their expensive steps on. It
appears in the checks list as **Shell Escape Residue Scan**.

Runs `scripts/check-shell-escape-residue.mjs`. It walks `AGENTS.md`, `CLAUDE.md`, every `.md`/`.mdx`
under `skills/` and every one under `content/docs/`, and fails when a **fenced code block** contains
one of the enumerated machine-produced shell-quote escape runs.

**Why it needed a gate.** In [#5150](https://github.com/objectstack-ai/objectui/issues/5150) the
`git commit -F -` example in `AGENTS.md` §9 shipped with its heredoc terminator wrapped in the
single-quote-inside-single-quote shell escape. Copied verbatim, that example does not fail with a
message — it **hangs**, on a terminator that never matches, and a reader does not attribute a hung
terminal to the document. [#5151](https://github.com/objectstack-ai/objectui/issues/5151) then ran the
full derived gate union against the replanted bytes: `check-control-bytes`, `check-doc-links`,
`check-changeset-presence` and `check-changeset-no-major` **all exited 0**. None of them was
negligent — the residue is printable ASCII inside a code block, and no scan surface in this repository
reached it. The amplifier is that `AGENTS.md`, `CLAUDE.md` and `skills/**` are re-read **once per
session** by every agent seat, so a bad example is not paid once; it is paid by every reader.

**⛔ What this gate does not do.** It checks an **enumerated literal** — one entry today, the sequence
#5150 leaked. It does **not** make fenced shell examples executable-by-construction, and nothing in
this repository does: a ```bash block may be syntactically invalid, may never terminate, or may name
a flag that does not exist, and this gate is green on all of it. Running `bash -n` over every block is
#5151's **unbuilt** "direction 1"; it was ruled out of that card rather than rejected on the merits,
and it carries a dependency worth recording — it is only as good as its **extraction convention**. In
#5150's own example the block sat inside a numbered list, so both lines carried a two-space indent,
and a quoted heredoc terminator must reach **column 0**. Rendered markdown strips the container indent
and the block looks fine; agents read these files by `cat`, not by rendering them, so a verbatim copy
including the indent hangs exactly as the original defect did. The boundary is asserted as a *fact* in
`scripts/__tests__/check-shell-escape-residue.test.ts` — broken shell is fed to the gate and a pass is
required — rather than pinned as a sentence, so the claim cannot rot into a false one.

**It is green at rest, so its census is part of the verdict.** There are zero occurrences in the tree
and there should stay zero, which means the run's output alone cannot distinguish a working gate from
one that matches nothing. The verdict line therefore prints the **per-root population** — files and
fenced blocks for each of the four roots — rather than a bare `OK`, and the scan **fails when that
population collapses**: a root that does not resolve, a root that walks to fewer documents than its
floor, or a total fence count under the floor is a broken walk, not a clean tree. A scan root that has
moved or been mistyped is reported **by name**, because a mistyped root and a clean root produce
identical output otherwise. The evidence that the gate works is the ablation in its test suite, which
replants #5150's exact line in each root on a fixture tree.

**Scope:** fenced blocks only. An occurrence in prose or an inline code span is **counted in the
census and not judged**, because documentation about this defect class has to be able to name the
literal. That is a known narrowing, and the census figure is what keeps it visible.

**If it fails:** it names the file, line and column, the fence language and the line the fence opened
on. Note that `AGENTS.md`, `CLAUDE.md` and `skills/**` are **governed surface** — a finding in one of
those is reported for a human to fix in its own change, not folded into an unrelated pull request. A
finding under `content/docs/**` is an ordinary docs fix. Run it locally with
`pnpm check:shell-escape-residue`, or `node scripts/check-shell-escape-residue.mjs --list` to see the
per-root census. It needs no install and no build.

## Link Checking (`check-links.yml`)

**Trigger:** Weekly cron (`17 4 * * 0` — Sundays, off the top of the hour, when the scheduled-run
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,7 @@
"check:entry-guard": "node scripts/check-entry-guard.mjs",
"check:pre-install-import-graph": "node scripts/check-pre-install-import-graph.mjs",
"check:vi-mock-specifiers": "node scripts/check-vi-mock-specifiers.mjs",
"check:shell-escape-residue": "node scripts/check-shell-escape-residue.mjs",
"cli": "node packages/cli/dist/cli.js",
"objectui": "node packages/cli/dist/cli.js",
"create-plugin": "node packages/create-plugin/dist/index.js",
Expand Down
Loading
Loading