Skip to content
Closed
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
4 changes: 3 additions & 1 deletion .github/GOVERNANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ AI-assisted PRs follow the same rules as any other. The author, not the agent, i

On every PR: the site builds, frontmatter namespaces and permalinks are present and unique, and the PR title matches the convention. A broken build or a duplicate permalink blocks the merge.

On every PR, informational only: a **webkit adoption report** runs the design-system ESLint rules over the UI and writes the result to the run Summary — how many `webkit/*` violations there are, which rules, which files, and what the check did *not* look at. It never blocks the merge. It exists so the distance between this codebase and the design system is a number someone can watch, instead of something noticed in review.
On every PR, the **Webkit gate** runs the design system's checks in four stages. Two of them block: the wiring must be sound, and a set of deliberately-broken fixtures must still be caught by the rules that guard them — that second one is what fails when the checks themselves stop working, which is otherwise silent. The other two report: an **adoption report** in the run Summary (how many `webkit/*` violations, which rules, which files, and what the check did *not* look at) and stylelint over the stylesheets. `Webkit gate` is the single check that aggregates them.

The adoption report never fails on the count it inherits. The violations that already exist are frozen in `.webkit-baseline.json`, and the stage fails only on one a PR **introduces** — naming the file and the rule. Fixing a frozen violation is reported, never punished; `pnpm report:webkit-adoption --update` prunes it from the baseline. So the number can only go down, and the gate never asks anyone to clean up the past before shipping.

Weekly: a link check crawls the built site for broken internal links and opens an issue when it finds them.

Expand Down
10 changes: 0 additions & 10 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,3 @@ jobs:
run: pnpm build:local
env:
NODE_OPTIONS: --max-old-space-size=8120

# Design-system adoption: measured, never enforced. The report lands in the run
# Summary; `if: always()` so it still appears when a step above fails, and
# `continue-on-error` so a red number never blocks the PR.
- name: Webkit adoption report
if: always()
continue-on-error: true
run: |
pnpm lint:webkit
pnpm --silent report:webkit-adoption >> "$GITHUB_STEP_SUMMARY"
160 changes: 160 additions & 0 deletions .github/workflows/webkit-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
name: Webkit gate

# The design-system gate, in four stages that are deliberately not equally strict.
#
# wiring — the toolkit is wired (catalog resolves, lint configs exist, MCP registered,
# nothing pinned to `latest`). Reports for now; see the note on the job.
# canary — fixtures that violate a rule on purpose must still be flagged by that exact
# rule. BLOCKS: it is the only stage that fails when the measurement itself
# stops working, and every way of losing the rules is otherwise silent.
# adoption — the score and the tables, written to the run Summary, then a ratchet: the
# frozen debt in .webkit-baseline.json stays green, and only a violation the
# PR ADDS fails the stage. BLOCKS on new, never on the existing count.
# style — the shipped stylelint config over CSS/SCSS/Vue/Astro. Reports.
#
# `Webkit gate` is the one check to mark required: it passes when every stage succeeded or
# was cleanly skipped.
#
# These stages run local scripts because the equivalent commands (`webkit report`,
# `webkit canary`) are being added to @aziontech/webkit itself — aziontech/webkit#964 — and
# only reach a consumer through an npm release. When that release lands, this whole file is
# replaced by one `uses:` of the design system's reusable workflow.

on:
pull_request:
branches:
- main
- release/new-azion-docs
types: [opened, synchronize, reopened]

concurrency:
group: webkit-gate-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read

jobs:
wiring:
name: Wiring
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: pnpm
- run: pnpm install --frozen-lockfile

# Not blocking yet, and for a specific reason: `webkit doctor` decides whether webkit
# is registered with Tailwind by looking only at `src/webkit.css`. This project does
# it correctly in `src/styles/main.css`, so the check reports a false FAIL. It becomes
# blocking as soon as the doctor reads the project's real CSS entry.
- name: Check the toolkit wiring
continue-on-error: true
run: pnpm doctor:webkit

canary:
name: Canary
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: pnpm
- run: pnpm install --frozen-lockfile

# Blocking. If this goes red the adoption number below is meaningless, not merely
# worse — the rules are not reaching the code at all.
- name: The rules still reach this project
run: pnpm canary:webkit

adoption:
name: Adoption report
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: pnpm
- run: pnpm install --frozen-lockfile

# The report is written first, unconditionally, so the number always reaches the
# Summary whatever the ratchet below decides.
- name: Measure and write the report
if: always()
run: pnpm --silent report:webkit-adoption >> "$GITHUB_STEP_SUMMARY"

- name: Keep the numbers as an artifact
if: always()
run: pnpm --silent report:webkit-adoption --format json > webkit-adoption.json
- uses: actions/upload-artifact@v4
if: always()
with:
name: webkit-adoption
path: webkit-adoption.json
if-no-files-found: warn

# The ratchet. The 72 violations in .webkit-baseline.json are frozen debt and stay
# green; this fails only on a violation that is NOT in that file — something the PR
# introduced. Fixing one is reported, never punished; re-snapshot with
# `pnpm report:webkit-adoption --update` to prune it.
- name: No new violations
run: pnpm --silent report:webkit-adoption --fail-on new > /dev/null

style:
name: Tokens in CSS
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Lint the stylesheets
continue-on-error: true
run: pnpm lint:style

webkit-gate:
name: Webkit gate
needs: [wiring, canary, adoption, style]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
# `skipped` counts as OK, so a stage can be disabled without changing which check is
# required.
- name: Check every stage passed or was skipped
run: |
declare -A results=(
[wiring]="${{ needs.wiring.result }}"
[canary]="${{ needs.canary.result }}"
[adoption]="${{ needs.adoption.result }}"
[style]="${{ needs.style.result }}"
)
failed=0
for stage in "${!results[@]}"; do
result="${results[$stage]}"
if [[ "$result" != "success" && "$result" != "skipped" ]]; then
echo "FAIL $stage: $result"
failed=1
else
echo "ok $stage: $result"
fi
done
if [[ $failed -eq 1 ]]; then
echo "The webkit gate did not pass. The adoption stage's Summary says where the project stands."
exit 1
fi
echo "Webkit gate passed."
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,3 @@ sandbox.config.json
!.claude/agents/
.codex/

# Intermediate ESLint JSON for the webkit adoption report.
.eslint-report.json
74 changes: 74 additions & 0 deletions .webkit-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
[
"src/components/Footer.vue::webkit/authoring-standards",
"src/components/Footer.vue::webkit/authoring-standards",
"src/components/Header.vue::webkit/authoring-standards",
"src/components/Header.vue::webkit/authoring-standards",
"src/components/Header.vue::webkit/no-style-override",
"src/components/Header.vue::webkit/no-style-override",
"src/components/Header.vue::webkit/no-style-override",
"src/components/Header.vue::webkit/no-style-override",
"src/components/Header.vue::webkit/no-style-override",
"src/components/RightSidebar/ContributeMenu.astro::webkit/no-hardcoded-color",
"src/components/RightSidebar/EditButton.astro::webkit/no-hardcoded-color",
"src/components/RightSidebar/EditButton.astro::webkit/no-hardcoded-color",
"src/components/SectionBasicContent/SectionBasicContent.vue::webkit/authoring-standards",
"src/components/SectionBasicContent/SectionBasicContent.vue::webkit/authoring-standards",
"src/components/agents/AgentFaq.vue::webkit/no-style-override",
"src/components/agents/AgentPageHeader.vue::webkit/no-hardcoded-color",
"src/components/agents/AgentToolsTable.vue::webkit/no-style-override",
"src/components/tabs/Tabs.vue::webkit/authoring-standards",
"src/components/webkit/AgentMark.vue::webkit/no-hardcoded-color",
"src/components/webkit/AgentMark.vue::webkit/no-hardcoded-color",
"src/components/webkit/AgentMark.vue::webkit/no-hardcoded-color",
"src/components/webkit/CodeBlock.vue::webkit/authoring-standards",
"src/components/webkit/Community.vue::webkit/authoring-standards",
"src/components/webkit/Community.vue::webkit/no-hardcoded-color",
"src/components/webkit/Community.vue::webkit/no-hardcoded-color",
"src/components/webkit/DocButton.vue::webkit/authoring-standards",
"src/components/webkit/DocPageHeader.vue::webkit/authoring-standards",
"src/components/webkit/DocsSidebar.vue::webkit/authoring-standards",
"src/components/webkit/DocsSidebar.vue::webkit/no-style-override",
"src/components/webkit/DocsSidebar.vue::webkit/no-style-override",
"src/components/webkit/DocsSidebar.vue::webkit/no-style-override",
"src/components/webkit/DocsSidebarFilter.vue::webkit/authoring-standards",
"src/components/webkit/DocsSidebarFilter.vue::webkit/no-style-override",
"src/components/webkit/DocsSidebarMenu.vue::webkit/authoring-standards",
"src/components/webkit/DocsSidebarMenu.vue::webkit/prefer-tree-shakeable-root",
"src/components/webkit/DocsTopNav.vue::webkit/no-style-override",
"src/components/webkit/DocsTopNav.vue::webkit/no-style-override",
"src/components/webkit/DocsTopNav.vue::webkit/no-style-override",
"src/components/webkit/DocsTopNav.vue::webkit/valid-import-path",
"src/components/webkit/HeaderRightSidebar.vue::webkit/authoring-standards",
"src/components/webkit/HeaderRightSidebar.vue::webkit/authoring-standards",
"src/components/webkit/HeaderRightSidebar.vue::webkit/no-hardcoded-color",
"src/components/webkit/HeaderRightSidebar.vue::webkit/no-hardcoded-color",
"src/components/webkit/HeaderRightSidebar.vue::webkit/no-hardcoded-color",
"src/components/webkit/HeaderRightSidebar.vue::webkit/no-style-override",
"src/components/webkit/HeaderRightSidebar.vue::webkit/no-style-override",
"src/components/webkit/HeaderRightSidebar.vue::webkit/no-style-override",
"src/components/webkit/HeaderSearch.vue::webkit/authoring-standards",
"src/components/webkit/HeaderSearch.vue::webkit/authoring-standards",
"src/components/webkit/HeaderSearch.vue::webkit/no-style-override",
"src/components/webkit/HeaderSearch.vue::webkit/no-style-override",
"src/components/webkit/HeaderSearchDialog.vue::webkit/authoring-standards",
"src/components/webkit/HeroHome.vue::webkit/authoring-standards",
"src/components/webkit/HeroHome.vue::webkit/no-style-override",
"src/components/webkit/OnThisPage.vue::webkit/no-style-override",
"src/components/webkit/ProductGuidesTable.vue::webkit/no-style-override",
"src/components/webkit/ProductGuidesTable.vue::webkit/no-style-override",
"src/components/webkit/ProductGuidesTable.vue::webkit/no-style-override",
"src/components/webkit/ReadableContent.vue::webkit/authoring-standards",
"src/components/webkit/ReadableContent.vue::webkit/no-style-override",
"src/components/webkit/SelectLang.vue::webkit/authoring-standards",
"src/components/webkit/SelectLang.vue::webkit/no-hardcoded-color",
"src/components/webkit/SelectLang.vue::webkit/no-hardcoded-color",
"src/components/webkit/SelectLang.vue::webkit/no-hardcoded-motion",
"src/components/webkit/SelectLang.vue::webkit/prefer-tree-shakeable-root",
"src/components/webkit/SystemStatus.vue::webkit/authoring-standards",
"src/components/webkit/SystemStatus.vue::webkit/no-hardcoded-motion",
"src/components/webkit/Tag.vue::webkit/authoring-standards",
"src/components/webkit/Tag.vue::webkit/authoring-standards",
"src/components/webkit/Tag.vue::webkit/no-style-override",
"src/i18n/en/header.ts::webkit/no-hardcoded-color",
"src/i18n/pt-br/header.ts::webkit/no-hardcoded-color"
]
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@
"nav:retarget": "tsm --require=./scripts/lib/filter-warnings.cjs ./scripts/nav/retarget-links.ts",
"lint:eslint": "eslint .",
"lint:style": "stylelint \"src/**/*.{css,scss,vue,astro}\"",
"lint:webkit": "eslint . -f json -o .eslint-report.json || true",
"report:webkit-adoption": "node scripts/webkit-adoption-report.mjs .eslint-report.json",
"report:webkit-adoption": "node scripts/webkit-adoption-report.mjs",
"canary:webkit": "node scripts/webkit-canary.mjs",
"doctor:webkit": "webkit doctor",
"translation-status": "tsm --require=./scripts/lib/filter-warnings.cjs ./scripts/translation-status.ts",
"cp-doc-helpcenter": "cp -R ../docs_help_center/* src/includes/help_center/en/",
"test:frontmatter": "node test-frontmatter.js"
Expand Down
83 changes: 83 additions & 0 deletions scripts/lib/webkit-lint.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Shared plumbing for the two webkit gate scripts.
//
// These scripts are a local stand-in for `webkit report` / `webkit canary`, which are being
// added to @aziontech/webkit itself (aziontech/webkit#964). They live here because the
// commands only reach a consumer through an npm release, and this repo needed the gate
// before that release. When the published package carries them, both scripts and this
// module go away and the CI calls the design system's own reusable workflow instead.

import { spawnSync } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { join } from 'node:path'

/** Extensions the design system governs — the denominator of the adoption score. */
export const UI_EXTENSIONS = ['vue', 'astro']

/**
* Run ESLint through this project's `node_modules/.bin/eslint` **shim**, never the Node API
* and never the resolved bin file.
*
* Measured here on 2026-09-10 with eslint 9.39.5, all three on the same files:
*
* new ESLint().lintFiles(['.']) → 0 findings in .astro (fatal parse error)
* node node_modules/…/eslint/bin/eslint.js → 0 findings in .astro (same failure)
* node_modules/.bin/eslint → 3 findings in .astro (correct)
*
* pnpm's shim exports NODE_PATH into its .pnpm directories before exec'ing node; without it
* ESLint cannot resolve astro-eslint-parser and silently falls back to the default parser.
* All three paths *succeed* — two just report a smaller number. A gate cannot be quietly
* wrong, so it uses the same entry point `pnpm lint:eslint` does.
*/
export function runESLint(cwd, patterns, extraArgs = []) {
const shim = join(
cwd,
'node_modules',
'.bin',
process.platform === 'win32' ? 'eslint.cmd' : 'eslint'
)
if (!existsSync(shim)) {
return { ok: false, reason: 'no node_modules/.bin/eslint — run pnpm install first' }
}

const proc = spawnSync(
shim,
[...patterns, '--format', 'json', '--no-error-on-unmatched-pattern', ...extraArgs],
{ cwd, encoding: 'utf-8', maxBuffer: 256 * 1024 * 1024 }
)
if (proc.error) return { ok: false, reason: proc.error.message }

// ESLint exits 1 when it finds errors, which is the normal case here. Only output that
// is not JSON means the run itself failed.
let results
try {
results = JSON.parse(proc.stdout)
} catch {
const detail = (proc.stderr || proc.stdout || '').trim().split('\n').slice(0, 6).join('\n')
return { ok: false, reason: `ESLint produced no JSON report (exit ${proc.status}).\n${detail}` }
}
if (!Array.isArray(results)) return { ok: false, reason: 'the ESLint report was not an array' }
return { ok: true, results }
}

/**
* The installed catalog is what the rules validate against. When it does not resolve the
* plugin disables eight of its twelve rules with a single stderr line — so "clean" and
* "blind" look identical unless something checks for it.
*/
export function readCatalog(cwd) {
try {
const require = createRequire(join(cwd, '__webkit__.js'))
const path = require.resolve('@aziontech/webkit/catalog.json')
const catalog = JSON.parse(readFileSync(path, 'utf-8'))
return { available: true, version: catalog.webkitVersion ?? null }
} catch {
return { available: false, version: null }
}
}

export function extensionOf(path) {
const base = path.slice(path.lastIndexOf('/') + 1)
const dot = base.lastIndexOf('.')
return dot === -1 ? '' : base.slice(dot + 1)
}
Loading
Loading