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
65 changes: 49 additions & 16 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ RUN apt-get update \
# HADOLINT_VERSION -> https://github.com/hadolint/hadolint/releases
# VALKEY_VERSION -> https://github.com/valkey-io/valkey/releases
# COSIGN_VERSION -> https://github.com/sigstore/cosign/releases
# NODE_MAJOR -> https://github.com/nodejs/node/releases (track a current LTS line)
# MARKDOWNLINT_CLI2_VERSION -> https://www.npmjs.com/package/markdownlint-cli2?activeTab=versions
# VALE_VERSION -> https://github.com/vale-cli/vale/releases

ENV PATH="/go/bin:/usr/local/go/bin:${PATH}" \
KUBECTL_VERSION=v1.36.2 \
Expand All @@ -73,7 +76,10 @@ ENV PATH="/go/bin:/usr/local/go/bin:${PATH}" \
ACTIONLINT_VERSION=1.7.12 \
HADOLINT_VERSION=2.14.0 \
VALKEY_VERSION=9.1.0 \
COSIGN_VERSION=v3.1.2
COSIGN_VERSION=v3.1.2 \
NODE_MAJOR=22 \
MARKDOWNLINT_CLI2_VERSION=0.23.1 \
VALE_VERSION=3.15.1

# Fail early on unsupported architectures instead of producing a partial image.
RUN test "$(dpkg --print-architecture)" = "amd64" \
Expand Down Expand Up @@ -176,6 +182,44 @@ RUN asset="cosign-linux-amd64" \
&& install -m 0755 "${asset}" /usr/local/bin/cosign \
&& rm -rf "${tmpdir}"

# Install Node.js (provides npm + npx).
#
# This lives in the ci stage, not dev-only, because markdownlint-cli2 below is a
# Node package and CI runs `task lint` *inside this stage* (see the `lint` job in
# .github/workflows/ci.yml). A docs linter installed only in the dev stage would
# pass locally and be missing in CI. The dev stage inherits it, so `npx` is still
# available for local use (e.g. installing Claude Code skills).
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& chmod a+r /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" \
> /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get -y install --no-install-recommends nodejs \
&& apt-get autoremove -y \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*

# Install markdownlint-cli2 (structural markdown linter: fences, headings, lists,
# tables). Ships as an npm package only -- there is no standalone binary release --
# so it is installed globally with the version pinned in the ENV block above.
RUN npm install -g "markdownlint-cli2@${MARKDOWNLINT_CLI2_VERSION}" \
&& npm cache clean --force

# Install Vale (prose linter). Enforces the house style in docs/style-guide.md via
# a repo-local style; no `vale sync` is needed and the image build stays hermetic.
# The Linux asset is named "64-bit" rather than "amd64", unlike every other tool here.
RUN asset="vale_${VALE_VERSION}_Linux_64-bit.tar.gz" \
&& base="https://github.com/vale-cli/vale/releases/download/v${VALE_VERSION}" \
&& tmpdir="$(mktemp -d)" \
&& curl -fsSL "${base}/${asset}" -o "${tmpdir}/${asset}" \
&& curl -fsSL "${base}/vale_${VALE_VERSION}_checksums.txt" -o "${tmpdir}/checksums.txt" \
&& cd "${tmpdir}" \
&& grep " ${asset}$" checksums.txt | sha256sum -c - \
&& tar -xzf "${asset}" vale \
&& install -m 0755 vale /usr/local/bin/vale \
&& rm -rf "${tmpdir}"

# Set working directory
WORKDIR /workspaces

Expand Down Expand Up @@ -253,15 +297,13 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# tooling, debugging), never in CI, so they live in the dev stage.
#
# Finding the latest versions:
# NODE_MAJOR -> https://github.com/nodejs/node/releases (track a current LTS line)
# TILT_VERSION -> https://github.com/tilt-dev/tilt/releases
# DLV_VERSION -> https://github.com/go-delve/delve/releases
# GOPLS_VERSION -> https://github.com/golang/tools/releases
# STATICCHECK_VERSION -> https://github.com/dominikh/go-tools/releases
ENV NODE_MAJOR=22 \
TILT_VERSION=v0.37.4 \
ENV TILT_VERSION=v0.37.4 \
DLV_VERSION=v1.27.0 \
GOPLS_VERSION=v0.22.0 \
GOPLS_VERSION=v0.23.0 \
STATICCHECK_VERSION=v0.7.0

# Kind is already installed in the ci stage above.
Expand Down Expand Up @@ -289,17 +331,8 @@ RUN printf '%s\n' \
'fi' \
>> /etc/bash.bashrc

# Install Node.js (provides npm + npx, e.g. for installing Claude Code skills).
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& chmod a+r /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" \
> /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get -y install --no-install-recommends nodejs \
&& apt-get autoremove -y \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*
# Node.js is installed in the ci stage (markdownlint-cli2 needs it there) and
# inherited here, so there is no dev-stage Node install.

# Install Tilt CLI (local dev loop orchestration; dev stage only)
RUN arch="$(dpkg --print-architecture)" && \
Expand Down
27 changes: 27 additions & 0 deletions .docs-lint-scope
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Markdown files the documentation linters gate.
#
# `task lint` fails on markdownlint findings or Vale errors in these files and
# ignores every other .md file in the tree. That is a rollout position, not the
# destination: measured with the committed config, 102 of 174 linted files fail
# markdownlint and 148 of 174 fail Vale, so gating everything today would be a
# wall no one could land a change through.
#
# Add a file here once it passes. Check what it would cost first:
#
# markdownlint-cli2 docs/some-file.md
# vale docs/some-file.md
# task lint-markdown-fix DOCS_SCOPE=all # the mechanical half, whole tree
#
# `task lint-markdown DOCS_SCOPE=all` and `task lint-prose DOCS_SCOPE=all` show
# the whole backlog without gating on it.
#
# Every path here must be tracked by git; a typo fails the lint run rather than
# silently shrinking the gate to nothing.
#
# Not in scope here: hack/doccheck, which always checks references across every
# tracked markdown, Go, YAML, and shell file. Only structure and prose are
# staged this way.

README.md
docs/architecture.md
docs/configuration.md
28 changes: 20 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ jobs:
golangci-lint version
actionlint --version
hadolint --version
markdownlint-cli2 --version
vale --version
controller-gen --version
k3d version
Comment thread
sunib marked this conversation as resolved.
docker --version
Expand Down Expand Up @@ -387,6 +389,8 @@ jobs:
flux-operator version --client
golangci-lint version
actionlint --version
markdownlint-cli2 --version
vale --version
k3d version
task --version
bash -ic 'complete -p task >/dev/null'
Expand Down Expand Up @@ -522,19 +526,27 @@ jobs:
fi
done

# lint-docs is exempt on purpose. It carries no `sources:` (see
# Taskfile-build.yml), because its real input is "every tracked .md, .go,
# .yml and .sh" — which Task cannot express: an unrooted `**` glob walks
# external-sources/ and has OOMed the host, and a hand-maintained file list
# silently drifts out of sync with what doccheck scans. A stale "up to date"
# there means the check never runs, so it always runs. It costs ~0.2s.
# The four docs tasks are exempt on purpose, and all for one reason: none
# of them carries `sources:` (see Taskfile-build.yml). Their real input is
# "every tracked .md" (plus .go, .yml and .sh for lint-doc-links) — which
# Task cannot express: an unrooted `**` glob walks external-sources/ and has
# OOMed the host, and a hand-maintained file list silently drifts out of
# sync with what the checkers scan. A stale "up to date" means the check
# never runs. All three tools together cost a few seconds over the whole
# tree, so they always run. lint-docs itself is the aggregate and has no
# cmds of its own.
#
# Adding a task to `task lint` without `sources:` and without adding it
# here fails this step. That is deliberate: it forces the choice to be made
# rather than silently skipped.
#
# awk, not `grep -qv`: the CI image ships ugrep as grep, whose -q -v returns
# 1 even when -c reports a selected line. That would make this assertion
# silently pass whatever it was handed.
uncached="$(awk "/^task: \\[/ && !/^task: \\[lint-docs\\]/" "${cache_log}")"
exempt="lint-docs|lint-doc-links|lint-markdown|lint-prose"
uncached="$(awk -v ex="^task: \\\\[(${exempt})\\\\]" "/^task: \\[/ && \$0 !~ ex" "${cache_log}")"
if [ -n "${uncached}" ]; then
echo "Expected second task lint dry-run to be fully cached apart from lint-docs, but these would run:" >&2
echo "Expected second task lint dry-run to be fully cached apart from the docs tasks, but these would run:" >&2
printf "%s\n" "${uncached}" >&2
exit 1
fi
Expand Down
76 changes: 76 additions & 0 deletions .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// markdownlint-cli2 configuration -- https://github.com/DavidAnson/markdownlint-cli2
//
// Structural markdown rules. Prose is Vale's job (.vale.ini); references are
// hack/doccheck's. See docs/design/docs-linting.md for what each choice cost
// when measured against the real corpus.
//
// `task lint-markdown` runs this, and `task lint` includes it, over the files
// .docs-lint-scope lists. Everything else is checked only when you ask:
// markdownlint-cli2 docs/some-file.md
// task lint-markdown DOCS_SCOPE=all
// task lint-markdown-fix DOCS_SCOPE=all
//
// Never point this at a bare recursive glob: external-sources/ holds gitignored
// upstream checkouts with symlink cycles that have OOM-killed the host once.
// Pass an explicit file list from `git ls-files`.
{
"config": {
// Wrap at 100 is a convention, not a gate (docs/style-guide.md). 120 costs
// 119 fixes and still catches a runaway paragraph; 100 costs 890 and would
// reformat most of docs/. Code and tables cannot be rewrapped.
"MD013": {
"line_length": 120,
"code_blocks": false,
"tables": false,
"headings": true
},

// The docs repeat headings like "Why" and "What it writes" under different
// parents on purpose. Only a clash between siblings is a real problem.
"MD024": { "siblings_only": true },

// 3,369 dash bullets to 155 asterisks. "consistent" would only catch the
// files that mix and leave the split in the tree; --fix repairs all 156.
"MD004": { "style": "dash" },

// docs/style-guide.md fixes the fence language set. Sorted by use; the last
// four are the ones the guide does not list yet (see the design doc's open
// questions). `sh` is deliberately absent: write `bash`.
"MD040": {
"allowed_languages": [
"bash",
"yaml",
"text",
"mermaid",
"go",
"promql",
"console",
"json",
"jsonc",
"http",
"gitignore",
"dockerfile"
]
},

"MD029": { "style": "ordered" },

// The demo GIF and the centered badge block in the root README.
"MD033": {
"allowed_elements": ["br", "details", "summary", "img", "div", "kbd", "sup"]
},

// Wants padded table cells; the repo writes |---|---| in 1,604 places and
// that is not worth a reflow of every table.
"MD060": false

// MD041 (first line must be a top-level heading) stays on. It catches six
// real files today, and the root README is the only one in the repo that
// opens with something other than prose or a heading. Its badge block earns
// an inline `markdownlint-disable-next-line` rather than a global opt-out.
},

// Generated by release-please, and a record of what happened rather than
// prose we maintain. Kept in sync with .vale.ini.
"ignores": ["CHANGELOG.md", "docs/finished/**", "external-sources/**", "node_modules/**"]
}
44 changes: 44 additions & 0 deletions .vale.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Vale configuration -- https://vale.sh/docs/vale-cli/structure/
#
# Prose linting against docs/style-guide.md. The rules live in
# .vale/styles/HouseStyle/ and are written for this repository; no external
# package is used and `vale sync` is never needed, so a lint run works offline.
# See docs/design/docs-linting.md for the measurements and the rollout plan.
#
# `task lint-prose` runs this, and `task lint` includes it, over the files
# .docs-lint-scope lists. Everything else is checked only when you ask:
# vale docs/some-file.md
# task lint-prose DOCS_SCOPE=all
#
# Vale skips fenced code blocks and inline code by default, which is what makes
# the "identifiers keep American spelling" tier of the style guide work without
# a single exception being listed. Two rules opt out of that with `scope: raw`
# and say why in their own headers: EmDash.yml and Correctives.yml.
StylesPath = .vale/styles

# Warnings and suggestions are informational. This is not a policy choice we get
# to make -- Vale's exit code tracks errors alone, so a run with warnings still
# exits 0. Verified, because it decides which level a rule needs to gate:
#
# error fails the gate once one exists. Mechanical rules only.
# warning informs an edit. Real but needs a human to weigh.
# suggestion a call a machine cannot make (Filler-Judgment.yml).
MinAlertLevel = suggestion

[*.md]
BasedOnStyles = HouseStyle

# CHANGELOG.md is generated by release-please, and docs/finished/ is a record of
# what happened rather than prose we maintain. Both are out of scope.
[CHANGELOG.md]
BasedOnStyles =

[docs/finished/*.md]
BasedOnStyles =

# Per-file rule exemptions go here, one stanza per file, because .vale.ini is
# applied above the scope machinery and is the only suppression that survives
# `scope: raw`. See EmDash.yml for when a file earns one. Empty on purpose today:
#
# [docs/style-guide.md]
# HouseStyle.EmDash = NO
34 changes: 34 additions & 0 deletions .vale/styles/HouseStyle/Correctives.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# docs/style-guide.md -> "The construction the dash is hiding".
#
# The corrective appositive, `X, not Y`, is not banned. Used once it lands; used
# in every third paragraph it becomes the tic readers point at when they say text
# sounds machine-written. The guide budgets roughly one per page.
#
# This is the only rule here that a human could not enforce by reading a diff,
# because the problem is the density and not any single instance.
#
# `scope: raw` makes the whole file one block, so this fires once per over-budget
# file rather than once per paragraph. "Roughly one per page" has to become a
# number, and files here run to several pages: max 3 flags 75 files, max 1 flags
# 110, max 5 flags 52. Three is the middle, and it is a warning, so it informs an
# edit rather than blocking one.
#
# The cost of `raw` is that a match inside a code block counts, and that guess has
# now been measured: 20 in-fence matches across 16 files, of which exactly two are
# pushed over budget by them and would otherwise sit at the limit --
# docs/spec/type-followability.md and
# test/fixtures/gitops-layouts/2-rendered/helm-environment-values/README.md.
# Two false alarms at warning level is cheaper than the alternative, which is
# giving up `raw` and counting per paragraph, where a budget of three is no budget
# at all. Left as is; do not re-litigate without new numbers.
#
# Note that unlike EmDash.yml, this rule needs no escape hatch. `raw` disables the
# in-file suppression comments there too, but a warning that fires once per file
# does not have to be silenced.
extends: occurrence
message: "Several 'X, not Y' correctives in one file. Budget roughly one per page, and prefer stating the correct thing positively."
link: https://github.com/ConfigButler/gitops-reverser/blob/main/docs/style-guide.md#the-construction-the-dash-is-hiding
level: warning
scope: raw
max: 3
token: ',\s+(?:not|never|rather than)\s'
38 changes: 38 additions & 0 deletions .vale/styles/HouseStyle/EmDash.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# docs/style-guide.md -> "Punctuation: no em dashes", the one hard rule.
#
# Vale skips code fences and inline code, so a dash inside a quoted specimen is
# left alone -- which is exactly what the style guide asks for. docs/style-guide.md
# itself is full of em dashes and is correctly never flagged.
#
# Known limitation: the reported count is a lower bound. A sentence with two
# dashes sometimes yields one alert. File-level detection is complete, so a gate
# is sound, but re-run after each pass instead of trusting one list.
#
# `scope: raw` was tried and reverted. It is tempting, because per-occurrence
# recall goes from 37% to 99.9% (measured over every tracked doc with fences and
# inline code stripped: 1,140 of 3,029 prose dashes at the default scope, 3,027
# at raw). It is still the wrong trade for a gate, for three measured reasons:
#
# 1. It costs the exemption above. Raw makes the whole file one block, so fences
# and inline code stop being skipped: 92 of 3,126 dashes (2.9%) across 32
# files, and style-guide.md flips from correctly clean to a false positive.
# 2. It cannot be suppressed. `<!-- vale off -->` and
# `<!-- vale HouseStyle.EmDash = NO -->` are markup-layer features that raw
# never sees, so the false positives above have no local escape hatch.
# 3. It does not buy gate correctness, because the default scope already has it.
# Over 148 files carrying a prose dash, the default scope misses none of them
# entirely and flags no clean file. Raw matches that and adds one false
# positive. Recall of the *count* is what improves, and the gate does not
# read the count.
#
# So: this rule decides whether a file is clean, and it is exact at that job. For
# the full list while you fix one, the style guide's checklist already ends with
# `grep '—' <file>`. That division is deliberate; do not re-litigate it with
# `scope:` without re-running those three measurements.
extends: existence
message: "Do not use an em dash in prose. Match the mark to the job: parentheses for an aside, a colon for a definition or expansion, a period or semicolon for two clauses."
link: https://github.com/ConfigButler/gitops-reverser/blob/main/docs/style-guide.md#punctuation-no-em-dashes
level: error
nonword: true
tokens:
- '—'
Loading