Skip to content

fix: preserve Vale severity in CI annotations and block merge on errors#26587

Draft
nickvigilante wants to merge 3 commits into
vigilante/docs-425-strip-third-party-vale-rules-enable-per-rule-only-viafrom
vigilante/docs-426-fix-vale-ci-annotation-severity-rendering-add-three-severity
Draft

fix: preserve Vale severity in CI annotations and block merge on errors#26587
nickvigilante wants to merge 3 commits into
vigilante/docs-425-strip-third-party-vale-rules-enable-per-rule-only-viafrom
vigilante/docs-426-fix-vale-ci-annotation-severity-rendering-add-three-severity

Conversation

@nickvigilante

@nickvigilante nickvigilante commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Closes DOCS-426. Stacked on #26586 (DOCS-425, strip).

Problem

Two coupled issues in the Vale CI step:

  1. Severity rendering: the Vale problem matcher at .github/vale-problem-matcher.json hard-codes "severity": "warning". Every Vale finding renders as a GitHub warning annotation, regardless of Vale's actual severity. Nick observed this on PR #25501: error-level findings from Coder.BrandNames appear as warnings.
  2. Advisory-only: the step ran with continue-on-error: true and vale --no-exit, so no Vale finding could fail the job, even at severity error. There was no merge gate on prose style.

Together these two issues collapsed the doctrine's three-severity ladder (error blocks merge, warning annotates without blocking, suggestion surfaces as a notice) into a single advisory channel. This PR restores the ladder.

Root cause (rendering)

GitHub Actions problem matchers expect either a regex capture group for severity or a hard-coded severity. Vale's --output=line format produces path:line:col:rule:message with severity stripped, so the matcher had no severity to capture and fell back on the hard-coded value.

Fix

Commit 1: severity rendering

Switch the Vale prose lint step to vale --output=JSON and pipe through jq to emit GitHub workflow commands directly. Drop the problem matcher file.

Vale severity GitHub workflow command
suggestion ::notice::
warning ::warning::
error ::error::

Message bodies are URL-encoded for %, \r, and \n per the GitHub Actions workflow command spec.

Commit 2: three-severity demo

Three throwaway Coder.Demo* rules at level: suggestion, level: warning, and level: error, plus a docs/.style/_vale-annotation-demo.md file that triggers each rule exactly once. Together with the rendering fix above, this PR's CI surfaces three GitHub annotations in three distinct severities (notice, warning, error). Use the Files Changed view to inspect rendering.

Commit 3: block merge on error-level findings

Drop continue-on-error: true and vale --no-exit from the Vale prose lint step. Error-level findings now exit non-zero, fail the step, fail the required lint-docs job, and block merge. Warning- and suggestion-level findings still render as annotations but do not affect the exit code. This matches Vale's native exit semantics (only errors trigger non-zero exit) and operationalizes the DOCS-425 doctrine: every rule lands clean (zero baseline findings) with a deliberate per-rule severity choice; error is the severity that catches new violations of hard policy and blocks the PR, while warning and suggestion annotate without forcing the gate.

set -o pipefail is required so Vale's exit code propagates through the jq pipe; without it, jq's success masks Vale's non-zero exit.

Expected CI state on this PR

The demo doc in commit 2 deliberately includes one error-level finding (Coder.DemoError). With commit 3 in place, this PR's lint-docs job is expected to fail with the demo error annotation rendered at line 21 of docs/.style/_vale-annotation-demo.md. That is the canary that proves the merge gate works end-to-end. Warning- and suggestion-level demo annotations render at lines 19 and 17 without affecting the exit code.

Local verification of the full pipeline:

$ set -o pipefail; printf '%s\n' 'docs/.style/_vale-annotation-demo.md' \
    | xargs -d '\n' vale --output=JSON \
    | jq -r '...'
::notice  file=docs/.style/_vale-annotation-demo.md,line=17,col=3,title=Coder.DemoSuggestion::[Demo] Suggestion-level Vale annotation.
::warning file=docs/.style/_vale-annotation-demo.md,line=19,col=3,title=Coder.DemoWarning::[Demo] Warning-level Vale annotation.
::error   file=docs/.style/_vale-annotation-demo.md,line=21,col=3,title=Coder.DemoError::[Demo] Error-level Vale annotation.
exit=1 (vale)  -> pipefail -> step exits non-zero -> job fails

Sequencing before merge

A follow-up commit on this branch drops the four demo files (docs/.style/_vale-annotation-demo.md and the three Coder.Demo* rules). After that drop, lint-docs goes green and the PR is mergeable.

Stacked on #26586. Targets the strip branch so the demo runs against a clean post-strip baseline (no third-party noise drowning the three demo annotations). After #26586 merges, this PR retargets to main.

Decision log
  • Workflow commands vs custom Vale template + updated matcher: chose workflow commands because the transform is a 10-line jq pipeline with no extra files to maintain, and it bypasses GitHub Actions problem-matcher limitations entirely. The custom-template option would have kept the matcher infrastructure but required an additional Go template file under .github/.
  • Throwaway demo rules vs reusing existing rules: chose throwaway because we wanted each severity to fire deterministically from a single unambiguous marker. Reusing existing rules would couple the demo to corpus content and obscure the signal.
  • Stacked PR vs independent: stacked because the visual demo is clearer against the clean post-strip baseline. The fix itself is independent and could land standalone, but the demo loses signal-to-noise if the underlying corpus is full of unrelated annotations.
  • docs/.style/_vale-annotation-demo.md filename: underscore prefix follows Coder convention for files that exist outside the normal docs taxonomy. Not surfaced on coder.com/docs because docs/.style/ is excluded from the manifest, deploy workflow, and docs preview.
  • Block merge on errors only, not warnings/suggestions: matches Vale's native exit semantics and the DOCS-425 doctrine's three-severity ladder. Severity gives the rule author a graduated tool: error for hard rules that must hold (e.g., Coder.BrandNames, banned first-person pronouns, em-dash bans), warning for strong guidance with legitimate human-judgment exceptions, suggestion for soft guidance where the right fix is contextual. Blocking on anything below error would force every rule onto an all-or-nothing setting and defeat the gradation the doctrine deliberately built.
  • set -o pipefail vs capturing exit then processing: pipefail is cleaner and keeps the streaming pipeline intact. The alternative (write to a temp file, capture exit code, jq from disk) trades one line of shell for an extra disk write and an extra variable to thread.

Filed via Coder Agents on Nick's behalf.

@linear-code

linear-code Bot commented Jun 22, 2026

Copy link
Copy Markdown

DOCS-426

@datadog-coder

datadog-coder Bot commented Jun 22, 2026

Copy link
Copy Markdown

Pipelines

⚠️ Warnings

🚦 3 Pipeline jobs failed

ci | lint-docs   View in Datadog   GitHub Actions

ci | required   View in Datadog   GitHub Actions

ci | test-js   View in Datadog   GitHub Actions

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: c774ad3 | Docs | Give us feedback!

@nickvigilante nickvigilante changed the title fix(.github/workflows/ci.yaml): preserve Vale severity in CI annotations fix: preserve Vale severity in CI annotations and add three-severity demo Jun 22, 2026
@nickvigilante nickvigilante changed the title fix: preserve Vale severity in CI annotations and add three-severity demo fix: preserve Vale severity in CI annotations and block merge on errors Jun 22, 2026
nickvigilante added a commit that referenced this pull request Jun 22, 2026
Collapse the Vale config to load only the Coder rule package. Drop the
Packages directive (no external packages sync), BasedOnStyles=Coder
only, drop every Google.X, write-good.X, and alex.X rule line. The
repo-root .vale.ini becomes a four-line config plus a doctrine comment.

Motivation: the previous config carried ~12,000 baseline findings
corpus-wide (412 errors / 5380 warnings / 6247 suggestions), almost
entirely from third-party rules whose false-positive patterns Vale
cannot distinguish from author intent. Google.Headings false-positives
on every acronym and product name (VM, AWS, Coder, Vale, JetBrains);
Google.Will fires on legitimate event-sequencing prose; alex.* rules
were enabled in DOCS-40 without a corpus cleanup commit. Engineers
stop reading annotations when CI cries wolf. The fix is a tight,
trustworthy ruleset rather than tuning around individual false
positives.

Doctrine added to docs/.style/README.md ('Adding a Vale rule'): every
rule ships clean (zero baseline findings) with a deliberate per-rule
severity choice. 'error' blocks merge in CI; 'warning' surfaces
annotations without failing CI; 'suggestion' surfaces 'notice'
annotations. The severity choice and the cleanup discipline are
independent, so a rule landing at any severity still ships with zero
baseline findings to keep the annotation channel trustworthy. Third-
party rules return via the same per-rule PR pattern after their corpus
is clean. See DOCS-425.

Effect on baseline: 412 errors -> 0, 5380 warnings -> 0, 6247
suggestions -> 0. Verified with 'mise exec aqua:errata-ai/vale -- vale
--no-exit docs/' against 465 files. Vale sync now skips the
third-party packages (the directories remain on disk for developers
who already synced; they're gitignored and no longer load). The CI
'Vale prose lint' step is currently advisory; PR #26587 (DOCS-426)
installs the merge gate that honors the doctrine's 'error blocks
merge' semantic.
@nickvigilante nickvigilante force-pushed the vigilante/docs-425-strip-third-party-vale-rules-enable-per-rule-only-via branch from 95f6409 to ba20bc2 Compare June 22, 2026 22:04
The Vale prose lint step in CI ran 'vale --output=line' through a GitHub Actions problem matcher at .github/vale-problem-matcher.json. The matcher hard-codes severity=warning at the top level, and Vale's line format strips per-finding severity, so every Vale finding rendered as a GitHub 'warning' annotation, even error-level findings from Coder.BrandNames.

Switch to 'vale --output=JSON' and pipe through jq to emit GitHub workflow commands directly. Mapping: Vale 'suggestion' -> ::notice::, Vale 'warning' -> ::warning::, Vale 'error' -> ::error::. URL-encode message bodies for '%', carriage return, and newline per the GitHub Actions workflow command spec.

Delete the now-unused .github/vale-problem-matcher.json. The CI step stays continue-on-error: true so Vale annotations remain advisory and never block merge. See DOCS-426.
Adds three throwaway Coder.Demo* rules and a single demo doc that triggers each rule exactly once. Together with the matcher fix in the previous commit, this PR's CI surfaces three GitHub annotations in three distinct severities (notice, warning, error). Used to visually verify that severity rendering works correctly after the fix.

Files:

- docs/.style/styles/Coder/DemoError.yml (level: error)

- docs/.style/styles/Coder/DemoWarning.yml (level: warning)

- docs/.style/styles/Coder/DemoSuggestion.yml (level: suggestion)

- docs/.style/_vale-annotation-demo.md (one trigger phrase per rule)

Each rule uses Vale's 'existence' extension type to match a single literal token (vale-demo-{error,warning,suggestion}-marker) that appears only in the demo doc. The demo doc carries a NOTE callout explaining its purpose.

Demo content drops in a follow-up commit on this branch before merge; only the matcher fix lands. Or, on @Vigilante's call, the demo stays as a permanent canary that monitors severity rendering across future CI changes. See DOCS-426.
…ings

Drop `continue-on-error: true` and `vale --no-exit` from the Vale prose
lint step so error-level findings fail the step, fail the required
`lint-docs` job, and block merge. Warning- and suggestion-level
findings still render as PR annotations but do not affect the exit
code, matching Vale's native exit semantics (only errors trigger
non-zero exit).

`set -o pipefail` is required so Vale's exit code propagates through
the jq pipe; without it, jq's success masks Vale's non-zero exit.

Pairs with DOCS-425's "rules ship clean" doctrine: any rule enabled in
`.vale.ini` ships with zero baseline error-level violations, so the
new merge gate fires only when a PR introduces new prose that
violates an enabled rule.

The three-severity demo doc deliberately includes one error-level
finding, so this PR's `lint-docs` job is expected to fail until the
demo content drops in the follow-up cleanup commit before merge.
@nickvigilante nickvigilante force-pushed the vigilante/docs-426-fix-vale-ci-annotation-severity-rendering-add-three-severity branch from 5aefebf to c774ad3 Compare June 22, 2026 22:07
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