From 3012180a8e7dfe47fafb2cfa8e19d887607a55e2 Mon Sep 17 00:00:00 2001 From: Isaque Santos Date: Thu, 10 Sep 2026 16:04:58 -0300 Subject: [PATCH 01/13] chore(ci): repair the deploy workflows and delete the dead one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three defects that had nothing to do with each other, all in the deploy lane: - `prod.yml` nested `workflow_dispatch` inside `push:`, so a manual production deploy was never actually possible. It is now a sibling trigger. - The Algolia credentials were passed as command-line arguments, which puts them in the runner's process list. They go through `env:` instead. - None of the three deploys declared `concurrency`, so two merges landing close together left `gsutil rsync -d` racing over the same bucket. Each environment now serialises, with `cancel-in-progress: false` — a deploy is never cancelled mid-flight. `azion-deploy.yml` is deleted. It was never runnable: the comment lines inside its steps block are tab-indented, which is a YAML parse error, and it ran `npm install` in a repository whose `preinstall` is `only-allow pnpm`. --- .github/workflows/azion-deploy.yml | 74 ------------------------------ .github/workflows/dev.yml | 8 ++++ .github/workflows/prod.yml | 25 ++++++---- .github/workflows/stage.yml | 8 ++++ 4 files changed, 32 insertions(+), 83 deletions(-) delete mode 100644 .github/workflows/azion-deploy.yml diff --git a/.github/workflows/azion-deploy.yml b/.github/workflows/azion-deploy.yml deleted file mode 100644 index 66e9022479..0000000000 --- a/.github/workflows/azion-deploy.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Deploy Application using Azion CLI - -on: - workflow_dispatch: - inputs: - branch: - required: true - type: choice - default: main - options: - - main - -jobs: - deploy: - name: Deploy - runs-on: ubuntu-latest - - permissions: - contents: write - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Use Node.js 20.x - uses: actions/setup-node@v4 - with: - node-version: 20 - - # If you want to change package manager, change the command below and add the necessary configurations - - name: Install dependencies - run: npm install - - - name: Install Azion CLI - run: | - curl -o azionlinux https://downloads.azion.com/linux/x86_64/azion - sudo mv azionlinux /usr/bin/azion - sudo chmod u+x /usr/bin/azion - - - name: CLI version - run: azion --version - - # Configure a personal token in your github secrets - # You may create a personal token by running 'azion create personal-token' - - name: Configure token - run: | - azion -t ${{ secrets.AZION_PERSONAL_TOKEN }} - azion whoami - - - name: Azion Build - run: | - azion build - - # You may add the --sync flag to sync local and remote resources - - name: Azion Deploy - run: | - azion deploy --local --debug - - - name: Commit Azion files - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add azion/azion.json azion.config.* || true - # Commit only if there are staged changes - if git diff --cached --quiet; then - echo "No Azion changes to commit. Skipping push." - else - git commit -m "chore: update azion files" - # Rebase in case remote has new commits, then push - git pull --rebase origin ${{ inputs.branch }} - git push - fi diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 555730b1db..8e18960113 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -3,6 +3,14 @@ on: push: branches: - dev + +concurrency: + group: deploy-dev + cancel-in-progress: false + +permissions: + contents: read + jobs: deploy: name: Development publising diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 4681b179a4..c42dfa528c 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -3,14 +3,16 @@ on: push: branches: - main - workflow_dispatch: - inputs: - branch: - required: true - type: choice - default: main - options: - - main + workflow_dispatch: + +# A deploy is never cancelled mid-flight: two merges landing close together would leave +# `gsutil rsync -d` racing over the same bucket. +concurrency: + group: deploy-production + cancel-in-progress: false + +permissions: + contents: read jobs: deploy: @@ -42,8 +44,13 @@ jobs: pnpm run build:prod - name: UPDATE Algolia data + # Through `env:`, not argv — a secret on the command line is readable in the + # runner's process list. + env: + ALGOLIA_APP: ${{ secrets.ALGOLIA_CONF_APP }} + ALGOLIA_API: ${{ secrets.ALGOLIA_CONF_API }} run: | - pnpm exec tsx cicd/algolia-reindex.ts app=${{ secrets.ALGOLIA_CONF_APP }} api=${{ secrets.ALGOLIA_CONF_API }} + pnpm exec tsx cicd/algolia-reindex.ts "app=$ALGOLIA_APP" "api=$ALGOLIA_API" - name: 'AUTH Google Cloud' uses: 'google-github-actions/auth@v2' diff --git a/.github/workflows/stage.yml b/.github/workflows/stage.yml index 26f3b19ee0..0b343f1421 100644 --- a/.github/workflows/stage.yml +++ b/.github/workflows/stage.yml @@ -3,6 +3,14 @@ on: push: branches: - stage + +concurrency: + group: deploy-stage + cancel-in-progress: false + +permissions: + contents: read + jobs: deploy: name: Development publising From 2414d2c64fe4cccf2d952fbbc1fe05e46a74f07d Mon Sep 17 00:00:00 2001 From: Isaque Santos Date: Thu, 10 Sep 2026 16:05:07 -0300 Subject: [PATCH 02/13] fix(linkcheck): point the checker at this site's base URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `lint:linkcheck` still carried `baseUrl: 'https://docs.astro.build'`, inherited from the Astro fork it descends from. The sitemap it reads is generated by Astro from `site`, which is `SITE_URL` — `http://localhost:4321` for a local build, `https://www.azion.com` for production. So the regex that pulls page paths out of `` matched nothing, every run parsed zero pages, and the checker reported "Found no link issues. Great job!" every time. That is worse than not having the check: the weekly workflow was green for a reason unrelated to the state of the links. With the base URL read from `SITE_URL` the checker parses all 1628 pages and reports real numbers. --- scripts/lint-linkcheck.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/lint-linkcheck.ts b/scripts/lint-linkcheck.ts index 6dc8fa42ac..cb2b0e6b30 100644 --- a/scripts/lint-linkcheck.ts +++ b/scripts/lint-linkcheck.ts @@ -1,3 +1,4 @@ +import { SITE_URL } from '../src/consts'; import { LinkCheckerState } from './lib/linkcheck/base/base'; import type { LinkCheckerOptions } from './lib/linkcheck/base/base'; import { CanonicalUrl } from './lib/linkcheck/checks/canonical-url'; @@ -51,7 +52,11 @@ class LinkChecker { } const linkChecker = new LinkChecker({ - baseUrl: 'https://docs.astro.build', + // The sitemap is generated by Astro from `site`, which is SITE_URL — so the base URL the + // checker matches against has to be the same one, per environment. It was still the + // upstream 'https://docs.astro.build' inherited from the Astro fork, which matches no + // in our sitemap: every run parsed zero pages and reported "no link issues". + baseUrl: SITE_URL, buildOutputDir: './dist', pageSourceDir: './src/content/docs', checks: [ From f163074b45a97ebe56ab2fe642ecd07a8cd29ee4 Mon Sep 17 00:00:00 2001 From: Isaque Santos Date: Thu, 10 Sep 2026 16:05:26 -0300 Subject: [PATCH 03/13] chore(ci): replace the PR workflows with a single aggregating gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PR gate was one job: a build, plus the frontmatter test that runs inside the build script. No lint, no typecheck, no format check, no link check, no slug check — although every one of those scripts is already written in this repository and simply never invoked. `ci-gate.yml` takes the shape of aziontech/webkit's `governance.yml`: a `changes` job with `dorny/paths-filter`, every job conditioned on one of its outputs, and one aggregator at the end. `CI gate` is the single check to mark required — it passes when every job succeeded or was cleanly skipped, so a job can be added or switched off without touching branch protection. The filter list includes the workflow and the lint configs themselves, so a PR that breaks the gate is not the one PR the gate never sees. What the jobs do: - `security` — `pnpm audit` and TruffleHog over the PR range. This repository is public and had no secret scanning at all. - `content` — the navigation tree blocks; translation slugs ratchet. - `lint` — ESLint, Prettier and Stylelint over the files the PR changed. Over the whole tree Prettier takes three minutes and reports 502 files, and ESLint reports 97 errors across 46. Neither number is any one branch's fault, and reformatting the repository to reach zero would bury every future diff. Scoping to the diff is the ratchet: what you touch, you leave clean. - `types` — `astro check`, ratcheted at the 54 errors already here. This is the class of defect that reaches production silently; DocFrame rendered an empty slot with a build reporting zero errors. - `build` — unchanged, except that it now packs the HTML and the sitemaps into an artifact. 730 MB of HTML compresses to ~94 MB, which is cheaper than a second five-minute build and is what a11y will consume next. - `linkcheck` — runs against that artifact instead of rebuilding, behind the fan-out guard so a red build skips it cleanly. Three checks are too far in debt to block outright, so they ratchet on a count frozen in `ci/baselines.json` (`scripts/ci/ratchet.mjs`): 54 type errors, 745 translations whose slug does not match the English page, and 21664 link issues. The count may fall, never rise. Most of those link issues are footer and header links to www.azion.com pages that live in the site repository rather than here — configuring that boundary is its own piece of work. A ratchet whose command stops producing a number fails rather than passing, so this cannot repeat what `lint:linkcheck` did for years. `pr-checks.yml` and `weekly-linkcheck.yml` are folded into the gate. The weekly one also filed no issues: its guard was `steps.linkcheck.conclusion == 'failure'` on a step with no `continue-on-error`, so the job aborted before reaching it. `pr-title.yml` is deleted. The `type(scope): summary` convention stays in GOVERNANCE.md and the PR template, upheld by the reviewer at squash time rather than by a bot that turns away an external contributor over a capital letter. --- .github/actions/setup/action.yml | 27 +++ .github/workflows/ci-gate.yml | 287 +++++++++++++++++++++++++ .github/workflows/pr-checks.yml | 53 ----- .github/workflows/pr-title.yml | 46 ---- .github/workflows/weekly-linkcheck.yml | 76 ------- ci/baselines.json | 20 ++ package.json | 3 +- scripts/ci/ratchet.mjs | 114 ++++++++++ 8 files changed, 450 insertions(+), 176 deletions(-) create mode 100644 .github/actions/setup/action.yml create mode 100644 .github/workflows/ci-gate.yml delete mode 100644 .github/workflows/pr-checks.yml delete mode 100644 .github/workflows/pr-title.yml delete mode 100644 .github/workflows/weekly-linkcheck.yml create mode 100644 ci/baselines.json create mode 100644 scripts/ci/ratchet.mjs diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 0000000000..4fa0dbfd31 --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,27 @@ +name: Setup +description: pnpm + Node from .nvmrc + a frozen install. The preamble every job repeats. + +inputs: + install: + description: Run `pnpm install --frozen-lockfile`. Set to 'false' for a job that only needs the checkout. + required: false + default: 'true' + +runs: + using: composite + steps: + - name: Set up pnpm + # No `version:` on purpose — it resolves from `packageManager` in package.json, + # so the lockfile and CI can never drift apart. + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + + - name: Set up Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .nvmrc + cache: pnpm + + - name: Install dependencies + if: inputs.install == 'true' + shell: bash + run: pnpm install --frozen-lockfile diff --git a/.github/workflows/ci-gate.yml b/.github/workflows/ci-gate.yml new file mode 100644 index 0000000000..884ff401c6 --- /dev/null +++ b/.github/workflows/ci-gate.yml @@ -0,0 +1,287 @@ +name: CI gate + +# The PR gate, in the shape of aziontech/webkit's governance.yml: a `changes` job with path +# filters, every job conditioned on one of its outputs, and one aggregator at the end. +# +# `CI gate` is the one check to mark required. It passes when every job succeeded or was +# cleanly skipped, so a job can be added or switched off without touching branch protection. +# +# This workflow never deploys. Deploys live in their own workflows, with their own secrets +# and the opposite concurrency (a deploy is never cancelled mid-flight). + +on: + pull_request: + branches: + - main + # The new-docs work lands here first, so PRs to it get the same gate. + - release/new-azion-docs + types: [opened, synchronize, reopened] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + changes: + name: Detect changes + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + content: ${{ steps.filter.outputs.content }} + platform: ${{ steps.filter.outputs.platform }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 + id: filter + with: + filters: | + # Touching the gate itself, or any config it enforces, re-runs everything — + # otherwise a PR that breaks the gate is the one PR the gate never sees. + gate: &gate + - '.github/workflows/ci-gate.yml' + - '.github/actions/setup/**' + - 'ci/baselines.json' + - 'scripts/ci/**' + - 'eslint.config.mjs' + - '.prettierrc*' + - '.prettierignore' + - '.stylelintrc*' + - '.nvmrc' + content: + - 'src/content/**' + - 'src/i18n/**' + - 'src/nav/**' + - 'cicd/massive-redirect/**' + - *gate + platform: + - 'src/**' + - 'plugins/**' + - 'integrations/**' + - 'scripts/**' + - 'astro.config.ts' + - 'tsconfig.json' + - 'postcss.config.js' + - 'package.json' + - 'pnpm-lock.yaml' + - *gate + + security: + name: Security scans + needs: changes + if: needs.changes.outputs.platform == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: ./.github/actions/setup + + - name: Audit dependencies + run: pnpm audit --audit-level high + + - name: Secret detection + uses: trufflesecurity/trufflehog@20652fbbdefffcdaa493a5bf57ab2ac6b1db715b # main + with: + path: ./ + base: ${{ github.event.pull_request.base.sha }} + head: ${{ github.event.pull_request.head.sha }} + extra_args: --only-verified + + content: + name: Content integrity + needs: changes + if: ${{ needs.changes.outputs.content == 'true' || needs.changes.outputs.platform == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: ./.github/actions/setup + + # Blocking: the navigation tree is the one structure a broken entry takes down for + # every page at once. + - name: Navigation tree + run: pnpm lint:navcheck + + # Ratchet. 745 translations already carry a slug that does not match the English + # page; this fails only on one this branch adds. + - name: Translation slugs (ratchet) + run: pnpm ci:ratchet slugcheck + + lint: + name: Lint & format + needs: changes + if: needs.changes.outputs.platform == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - uses: ./.github/actions/setup + + # Changed files only, on purpose. Running Prettier over the whole tree takes three + # minutes and reports 502 files; ESLint reports 97 errors across 46. Neither number + # is this branch's fault, and neither is worth reformatting the repository to reach. + # Scoping to the diff is the ratchet: what you touch, you leave clean. + - name: Collect changed files + id: diff + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + git diff --name-only --diff-filter=ACMR "$BASE_SHA" "$HEAD_SHA" > changed.txt + grep -E '\.(js|mjs|cjs|ts|tsx|vue|astro)$' changed.txt > changed-code.txt || true + grep -E '\.(css|scss|vue|astro)$' changed.txt > changed-style.txt || true + grep -E '\.(js|mjs|cjs|ts|tsx|vue|astro|json|md|mdx|yml|yaml|css|scss)$' changed.txt > changed-fmt.txt || true + echo "code=$(wc -l < changed-code.txt)" >> "$GITHUB_OUTPUT" + echo "style=$(wc -l < changed-style.txt)" >> "$GITHUB_OUTPUT" + echo "fmt=$(wc -l < changed-fmt.txt)" >> "$GITHUB_OUTPUT" + + - name: ESLint (zero warnings on changed files) + if: steps.diff.outputs.code != '0' + run: tr '\n' '\0' < changed-code.txt | xargs -0 pnpm exec eslint --max-warnings 0 + + - name: Prettier (changed files) + if: steps.diff.outputs.fmt != '0' + run: tr '\n' '\0' < changed-fmt.txt | xargs -0 pnpm exec prettier --check + + - name: Stylelint (changed files) + if: steps.diff.outputs.style != '0' + run: tr '\n' '\0' < changed-style.txt | xargs -0 pnpm exec stylelint + + types: + name: Types + needs: changes + if: needs.changes.outputs.platform == 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: ./.github/actions/setup + + # Ratchet, for the same reason as the slugs: 54 type errors are already here. This is + # the class of bug that reaches production silently — DocFrame rendered an empty slot + # with a build reporting zero errors. + - name: astro check (ratchet) + run: pnpm ci:ratchet astro-check + + webkit: + name: Design system adoption + needs: changes + if: needs.changes.outputs.platform == 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: ./.github/actions/setup + + # Measured, not enforced — for now. The four-stage gate (wiring · canary · adoption + # ratchet · style) lives on chore/webkit-gate and replaces this step when it lands. + - name: Adoption report + if: always() + continue-on-error: true + run: | + pnpm lint:webkit + pnpm --silent report:webkit-adoption >> "$GITHUB_STEP_SUMMARY" + + build: + name: Build + needs: changes + if: ${{ !cancelled() }} + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: ./.github/actions/setup + + # `build:local` ends in test:frontmatter, so namespaces and unique permalinks are + # asserted here. + - name: Build and validate frontmatter + run: pnpm build:local + env: + NODE_OPTIONS: --max-old-space-size=8120 + + # The link checker reads only the sitemaps and the HTML — not the 97 MB of assets. + # 730 MB of HTML packs down to ~94 MB, which is cheaper than a second five-minute + # build, and it is what a11y will consume next. + - name: Pack the HTML for the jobs that consume it + run: | + find dist \( -name '*.html' -o -name 'sitemap-*.xml' \) -type f > dist-files.txt + tar -czf dist-html.tar.gz -T dist-files.txt + ls -lh dist-html.tar.gz + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: dist-html + path: dist-html.tar.gz + retention-days: 1 + compression-level: 0 + + linkcheck: + name: Internal links + needs: [changes, build] + # The fan-out guard: with the build red there is nothing to check, so skip cleanly + # instead of burning a runner. + if: ${{ !cancelled() && !contains(needs.*.result, 'failure') }} + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: ./.github/actions/setup + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: dist-html + + - name: Unpack the build output + run: tar -xzf dist-html.tar.gz + + # Ratchet. The checker had been pointed at the wrong base URL since the Astro fork, + # so it parsed zero pages and always said "no link issues". With the URL fixed it + # reports real numbers, and most of them are links to www.azion.com pages that live + # in the site repo, not here — which is a configuration job, not this branch's. + - name: Link check (ratchet) + run: pnpm ci:ratchet linkcheck + + ci-gate: + name: CI gate + needs: [changes, security, content, lint, types, webkit, build, linkcheck] + if: always() + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + # `skipped` counts as OK, so a job the path filter did not select — or one switched + # off — never blocks a merge. + - name: Every job passed or was skipped + run: | + declare -A results=( + [changes]="${{ needs.changes.result }}" + [security]="${{ needs.security.result }}" + [content]="${{ needs.content.result }}" + [lint]="${{ needs.lint.result }}" + [types]="${{ needs.types.result }}" + [webkit]="${{ needs.webkit.result }}" + [build]="${{ needs.build.result }}" + [linkcheck]="${{ needs.linkcheck.result }}" + ) + failed=0 + for job in "${!results[@]}"; do + result="${results[$job]}" + if [[ "$result" != "success" && "$result" != "skipped" ]]; then + echo "FAIL $job: $result" + failed=1 + else + echo "ok $job: $result" + fi + done + if [[ $failed -eq 1 ]]; then + echo "The CI gate did not pass." + exit 1 + fi + echo "CI gate passed." diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml deleted file mode 100644 index 9f2d182ec7..0000000000 --- a/.github/workflows/pr-checks.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: PR checks -on: - pull_request: - branches: - - main - # The new-docs work lands here first, so PRs to it need the same checks. - - release/new-azion-docs - types: [opened, synchronize, reopened] - -concurrency: - group: pr-checks-${{ github.event.pull_request.number }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - validate: - name: Build and frontmatter - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up pnpm - uses: pnpm/action-setup@v4 - - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version: 22.23.2 - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Validate navigation - run: pnpm lint:navcheck - - - name: Build and validate frontmatter - 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" diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml deleted file mode 100644 index d7edc74737..0000000000 --- a/.github/workflows/pr-title.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: PR title -on: - pull_request: - types: [opened, edited, reopened, synchronize] - -concurrency: - group: pr-title-${{ github.event.pull_request.number }} - cancel-in-progress: true - -permissions: - pull-requests: read - -jobs: - title: - name: Conventional title, no ticket codes - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Check type(scope) format - uses: amannn/action-semantic-pull-request@v5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - types: | - feat - fix - docs - i18n - refactor - chore - requireScope: true - # Imperative summaries start lowercase. This is what rejects - # "Enhance documentation on..." and "Update en.json". - subjectPattern: ^(?![A-Z]).+$ - subjectPatternError: > - The summary "{subject}" must start with a lowercase letter and read as an - imperative, for example "clarify Bot Manager scoring rules". - - - name: Reject ticket codes in the title - env: - PR_TITLE: ${{ github.event.pull_request.title }} - run: | - if printf '%s' "$PR_TITLE" | grep -qE '^\[?[A-Z]{2,}-[0-9]+\]?|\[[A-Z]{2,}-[0-9]+\]'; then - echo "::error::Ticket codes do not belong in PR titles. Put the ticket in the branch name (EDU-1234-short-slug) and the Related issue field in the PR body." - exit 1 - fi diff --git a/.github/workflows/weekly-linkcheck.yml b/.github/workflows/weekly-linkcheck.yml deleted file mode 100644 index a7ce717aaf..0000000000 --- a/.github/workflows/weekly-linkcheck.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: Weekly link check -on: - schedule: - - cron: '0 6 * * 1' # Mondays 06:00 UTC, 03:00 GMT-3 - workflow_dispatch: - -permissions: - contents: read - issues: write - -jobs: - linkcheck: - name: Internal links - runs-on: ubuntu-latest - timeout-minutes: 45 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up pnpm - uses: pnpm/action-setup@v4 - - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version: 20.13.1 - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Check internal links - id: linkcheck - run: | - pnpm build:local - pnpm exec tsm --require=./scripts/lib/filter-warnings.cjs ./scripts/lint-linkcheck.ts - env: - SKIP_OG: 'true' - NODE_OPTIONS: --max-old-space-size=8120 - - - name: File an issue on breakage - if: steps.linkcheck.conclusion == 'failure' - uses: actions/github-script@v7 - with: - script: | - const label = 'broken-links'; - const existing = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - labels: label, - }); - const run = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - const open = existing.data.filter((i) => !i.pull_request); - if (open.length > 0) { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: open[0].number, - body: `Still failing as of the latest run: ${run}`, - }); - return; - } - await github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: 'Weekly link check is failing', - labels: [label], - body: [ - 'The weekly internal link check failed.', - '', - `Run: ${run}`, - '', - 'This issue stays open and collects a comment per failing run. Close it once the run is green.', - ].join('\n'), - }); diff --git a/ci/baselines.json b/ci/baselines.json new file mode 100644 index 0000000000..377cb73a42 --- /dev/null +++ b/ci/baselines.json @@ -0,0 +1,20 @@ +{ + "astro-check": { + "command": "pnpm -s check", + "pattern": "-\\s+(\\d+)\\s+errors", + "baseline": 54, + "label": "erros de tipo" + }, + "slugcheck": { + "command": "pnpm -s lint:slugcheck", + "pattern": "Found (\\d+) translations with mismatched slugs", + "baseline": 745, + "label": "traduções com slug divergente do inglês" + }, + "linkcheck": { + "command": "pnpm -s lint:linkcheck:nobuild", + "pattern": "Found (?:(\\d+)|no) link issues", + "baseline": 21664, + "label": "links quebrados no build" + } +} diff --git a/package.json b/package.json index 272b07da39..821ebcc01e 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,9 @@ "lint:a11y:local": "pa11y-ci --sitemap 'http://localhost:3000/sitemap.xml' --sitemap-find 'https://www.azion.com' --sitemap-replace 'http://localhost:3000'", "lint:a11y:remote": "pa11y-ci --sitemap 'https://wwww.azion.com/sitemap.xml'", "lint:linkcheck": "SKIP_OG=true pnpm run build && tsm --require=./scripts/lib/filter-warnings.cjs ./scripts/lint-linkcheck.ts", - "lint:linkcheck:nobuild": "tsm --require=./scripts/lib/filter-warnings.cjs ./scripts/lint-linkcheck.ts", + "lint:linkcheck:nobuild": "NODE_OPTIONS=--max-old-space-size=8120 tsm --require=./scripts/lib/filter-warnings.cjs ./scripts/lint-linkcheck.ts", "lint:slugcheck": "node ./scripts/lint-slugcheck.mjs", + "ci:ratchet": "node scripts/ci/ratchet.mjs", "lint:navcheck": "tsm --require=./scripts/lib/filter-warnings.cjs ./scripts/lint-navcheck.ts", "nav:url-map": "tsm --require=./scripts/lib/filter-warnings.cjs ./scripts/nav/url-map.ts", "nav:migrate": "tsm --require=./scripts/lib/filter-warnings.cjs ./scripts/nav/migrate-permalinks.ts", diff --git a/scripts/ci/ratchet.mjs b/scripts/ci/ratchet.mjs new file mode 100644 index 0000000000..32eff59e38 --- /dev/null +++ b/scripts/ci/ratchet.mjs @@ -0,0 +1,114 @@ +#!/usr/bin/env node +// A count ratchet for checks whose debt is too large to fix before turning them on. +// +// Each check runs a command, extracts one number from its output, and compares that number +// with the frozen baseline in ci/baselines.json. The number may go down (the baseline is +// then stale, which is reported, never punished) but never up. It is the same idea as the +// webkit adoption baseline, applied to tools that have no baseline mechanism of their own. +// +// node scripts/ci/ratchet.mjs check +// node scripts/ci/ratchet.mjs --update re-snapshot the baseline +// node scripts/ci/ratchet.mjs --all check every entry + +import { execSync } from 'node:child_process'; +import { readFileSync, writeFileSync, appendFileSync } from 'node:fs'; + +const BASELINES = 'ci/baselines.json'; + +function run(command) { + try { + return strip( + execSync(command, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + maxBuffer: 256 * 1024 * 1024, + }) + ); + } catch (error) { + // These commands exit non-zero precisely because they found something. The output, + // not the exit code, is the measurement. + return strip(`${error.stdout ?? ''}${error.stderr ?? ''}`); + } +} + +// astro check and the link checker colour their output; the counts we match sit between +// escape sequences. +function strip(text) { + // eslint-disable-next-line no-control-regex + return text.replace(/\u001b\[[0-9;]*m/g, ''); +} + +function summary(line) { + if (process.env.GITHUB_STEP_SUMMARY) appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${line}\n`); +} + +function check(name, entry, update) { + const output = run(entry.command); + const match = output.match(new RegExp(entry.pattern)); + + if (!match) { + // A check that stops producing its own number is not "clean" — it is broken, and + // silently passing would be the worst outcome. `lint:linkcheck` spent its whole life + // matching a base URL that appears in no sitemap and reporting zero issues. + console.error(`FAIL ${name}: could not read a count from the output of \`${entry.command}\`.`); + console.error(output.split('\n').slice(-25).join('\n')); + return false; + } + + // A pattern may match a "none found" phrasing with no digits — that is a real zero. + const count = match[1] === undefined ? 0 : Number(match[1]); + + if (update) { + entry.baseline = count; + console.log(`ok ${name}: baseline set to ${count}`); + return true; + } + + if (count > entry.baseline) { + console.error( + `FAIL ${name}: ${count} ${entry.label} — the baseline is ${ + entry.baseline + }, so this branch adds ${count - entry.baseline}.` + ); + console.error(output.split('\n').slice(-40).join('\n')); + summary(`| ${name} | **${count}** | ${entry.baseline} | +${count - entry.baseline} |`); + return false; + } + + const delta = entry.baseline - count; + console.log( + `ok ${name}: ${count} ${entry.label} (baseline ${entry.baseline}${ + delta ? `, ${delta} fewer` : '' + })` + ); + summary(`| ${name} | ${count} | ${entry.baseline} | ${delta ? `−${delta}` : '—'} |`); + return true; +} + +const args = process.argv.slice(2); +const update = args.includes('--update'); +const names = args.filter((a) => !a.startsWith('--')); +const baselines = JSON.parse(readFileSync(BASELINES, 'utf8')); +const selected = names.length ? names : Object.keys(baselines); + +for (const name of selected) { + if (!baselines[name]) { + console.error(`FAIL unknown check "${name}". Known: ${Object.keys(baselines).join(', ')}`); + process.exit(1); + } +} + +summary('| check | agora | baseline | delta |'); +summary('|---|---|---|---|'); + +let failed = false; +for (const name of selected) { + if (!check(name, baselines[name], update)) failed = true; +} + +if (update) { + writeFileSync(BASELINES, `${JSON.stringify(baselines, null, 2)}\n`); + console.log(`\n${BASELINES} updated.`); +} + +process.exit(failed ? 1 : 0); From fe66716b797f06f9698a879274ab3f5a8edb0eb9 Mon Sep 17 00:00:00 2001 From: Isaque Santos Date: Thu, 10 Sep 2026 16:08:37 -0300 Subject: [PATCH 04/13] chore(ci): ratchet the dependency audit instead of blocking on it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pnpm audit --audit-level high` fails on this tree: 33 high and 1 critical, none of them introduced by any branch — the repository has simply never run an audit. Blocking outright would mean fixing all 34 before any PR could merge, which is the same trap the type errors and the slug mismatches are in. So it joins them: `scripts/ci/audit-count.mjs` reports high + critical from `pnpm audit --json`, and the count ratchets at 34. It reads the JSON metadata rather than the table because the table omits severities that are at zero — a regex for "critical" would stop matching the day the last one is fixed, and a ratchet that stops matching fails. Worth naming, since the audit surfaced it: the critical is astro < 7.2.8, RCE through AVIF image optimization, and this repository is on ^7.2.3. Bumping Astro is its own change with its own build risk, not a side effect of wiring up CI. --- .github/workflows/ci-gate.yml | 8 ++++++-- ci/baselines.json | 6 ++++++ scripts/ci/audit-count.mjs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 scripts/ci/audit-count.mjs diff --git a/.github/workflows/ci-gate.yml b/.github/workflows/ci-gate.yml index 884ff401c6..030ffceab0 100644 --- a/.github/workflows/ci-gate.yml +++ b/.github/workflows/ci-gate.yml @@ -82,8 +82,12 @@ jobs: persist-credentials: false - uses: ./.github/actions/setup - - name: Audit dependencies - run: pnpm audit --audit-level high + # Ratchet. 33 high and 1 critical are already in the tree — the repository has never + # run an audit, so blocking outright would mean fixing all 34 before any PR could + # merge. The critical one is astro < 7.2.8 (RCE through AVIF image optimization); + # bumping Astro is its own change, not a side effect of wiring up CI. + - name: Audit dependencies (ratchet) + run: pnpm ci:ratchet audit - name: Secret detection uses: trufflesecurity/trufflehog@20652fbbdefffcdaa493a5bf57ab2ac6b1db715b # main diff --git a/ci/baselines.json b/ci/baselines.json index 377cb73a42..f1d4fc11cc 100644 --- a/ci/baselines.json +++ b/ci/baselines.json @@ -1,4 +1,10 @@ { + "audit": { + "command": "node scripts/ci/audit-count.mjs", + "pattern": "high\\+critical: (\\d+)", + "baseline": 34, + "label": "vulnerabilidades high/critical nas dependências" + }, "astro-check": { "command": "pnpm -s check", "pattern": "-\\s+(\\d+)\\s+errors", diff --git a/scripts/ci/audit-count.mjs b/scripts/ci/audit-count.mjs new file mode 100644 index 0000000000..c69fa807b9 --- /dev/null +++ b/scripts/ci/audit-count.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env node +// Counts high + critical advisories from `pnpm audit --json` and prints one line the +// ratchet can read. Parsing the JSON metadata beats a regex over the table: the table +// omits severities with a count of zero, so a pattern for "critical" would stop matching +// on the day the last critical is fixed — and a ratchet that stops matching fails. + +import { execSync } from 'node:child_process'; + +let raw = ''; +try { + raw = execSync('pnpm audit --json', { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }); +} catch (error) { + // `pnpm audit` exits non-zero when it finds anything; the report is still on stdout. + raw = `${error.stdout ?? ''}`; +} + +let counts; +try { + counts = JSON.parse(raw).metadata.vulnerabilities; +} catch { + console.error('Could not parse `pnpm audit --json`.'); + console.error(raw.slice(0, 2000)); + process.exit(1); +} + +const blocking = (counts.high ?? 0) + (counts.critical ?? 0); +const other = (counts.moderate ?? 0) + (counts.low ?? 0) + (counts.info ?? 0); + +console.log(`high+critical: ${blocking}`); +console.log(`(moderate/low/info: ${other})`); From 5e4bfd116faf5bc4e0acc423f359797bf3ee8d3a Mon Sep 17 00:00:00 2001 From: Isaque Santos Date: Thu, 10 Sep 2026 16:10:48 -0300 Subject: [PATCH 05/13] chore(ci): drop the dead initializer the gate caught in audit-count --- scripts/ci/audit-count.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci/audit-count.mjs b/scripts/ci/audit-count.mjs index c69fa807b9..5b831432a4 100644 --- a/scripts/ci/audit-count.mjs +++ b/scripts/ci/audit-count.mjs @@ -6,7 +6,7 @@ import { execSync } from 'node:child_process'; -let raw = ''; +let raw; try { raw = execSync('pnpm audit --json', { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }); } catch (error) { From e08206b4d800c4ef29de197b256b10b63e4b6e43 Mon Sep 17 00:00:00 2001 From: Isaque Santos Date: Thu, 10 Sep 2026 16:36:58 -0300 Subject: [PATCH 06/13] chore(ci): move internal links and design-system adoption into their own workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both were jobs inside `ci-gate.yml`. They are now `on: workflow_call` workflows of their own, called from the gate with `uses: ./.github/workflows/…`. Being called rather than triggered is what keeps them useful: a called workflow runs inside the caller's run, so Internal links still downloads the artifact the build produced instead of building the site a second time — 8 s of download and unpack against 167 s of rebuild — and `CI gate` is still the single check to mark required, because a call reports its result through `needs` like any job. What the split buys is that each one can move. `internal-links.yml` takes the artifact name as an input, so it is not tied to this graph. And `design-system-adoption.yml` is the seam with the design system: the four-stage gate on chore/webkit-gate replaces its body, and eventually the whole file becomes one `uses:` of @aziontech/webkit's own reusable workflow — churn that now happens outside ci-gate.yml, which stays still. The aggregator reads the two results with bracket notation (`needs['internal-links'].result`); a hyphenated job id after a dot is ambiguous with subtraction in the expression grammar. --- .github/workflows/ci-gate.yml | 62 +++++++------------- .github/workflows/design-system-adoption.yml | 31 ++++++++++ .github/workflows/internal-links.yml | 45 ++++++++++++++ 3 files changed, 98 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/design-system-adoption.yml create mode 100644 .github/workflows/internal-links.yml diff --git a/.github/workflows/ci-gate.yml b/.github/workflows/ci-gate.yml index 030ffceab0..5f4412f2d6 100644 --- a/.github/workflows/ci-gate.yml +++ b/.github/workflows/ci-gate.yml @@ -6,6 +6,11 @@ name: CI gate # `CI gate` is the one check to mark required. It passes when every job succeeded or was # cleanly skipped, so a job can be added or switched off without touching branch protection. # +# Two checks live in their own files and are called from here: `internal-links.yml` and +# `design-system-adoption.yml`. Both are `on: workflow_call`, so they run inside this run +# — which is what lets Internal links download the artifact the build produced — while +# staying movable on their own. +# # This workflow never deploys. Deploys live in their own workflows, with their own secrets # and the opposite concurrency (a deploy is never cancelled mid-flight). @@ -175,24 +180,10 @@ jobs: - name: astro check (ratchet) run: pnpm ci:ratchet astro-check - webkit: - name: Design system adoption + design-system-adoption: needs: changes if: needs.changes.outputs.platform == 'true' - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: ./.github/actions/setup - - # Measured, not enforced — for now. The four-stage gate (wiring · canary · adoption - # ratchet · style) lives on chore/webkit-gate and replaces this step when it lands. - - name: Adoption report - if: always() - continue-on-error: true - run: | - pnpm lint:webkit - pnpm --silent report:webkit-adoption >> "$GITHUB_STEP_SUMMARY" + uses: ./.github/workflows/design-system-adoption.yml build: name: Build @@ -227,35 +218,26 @@ jobs: retention-days: 1 compression-level: 0 - linkcheck: - name: Internal links + internal-links: needs: [changes, build] # The fan-out guard: with the build red there is nothing to check, so skip cleanly # instead of burning a runner. if: ${{ !cancelled() && !contains(needs.*.result, 'failure') }} - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: ./.github/actions/setup - - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: dist-html - - - name: Unpack the build output - run: tar -xzf dist-html.tar.gz - - # Ratchet. The checker had been pointed at the wrong base URL since the Astro fork, - # so it parsed zero pages and always said "no link issues". With the URL fixed it - # reports real numbers, and most of them are links to www.azion.com pages that live - # in the site repo, not here — which is a configuration job, not this branch's. - - name: Link check (ratchet) - run: pnpm ci:ratchet linkcheck + uses: ./.github/workflows/internal-links.yml ci-gate: name: CI gate - needs: [changes, security, content, lint, types, webkit, build, linkcheck] + needs: + [ + changes, + security, + content, + lint, + types, + design-system-adoption, + build, + internal-links + ] if: always() runs-on: ubuntu-latest timeout-minutes: 5 @@ -270,9 +252,9 @@ jobs: [content]="${{ needs.content.result }}" [lint]="${{ needs.lint.result }}" [types]="${{ needs.types.result }}" - [webkit]="${{ needs.webkit.result }}" + [design-system-adoption]="${{ needs['design-system-adoption'].result }}" [build]="${{ needs.build.result }}" - [linkcheck]="${{ needs.linkcheck.result }}" + [internal-links]="${{ needs['internal-links'].result }}" ) failed=0 for job in "${!results[@]}"; do diff --git a/.github/workflows/design-system-adoption.yml b/.github/workflows/design-system-adoption.yml new file mode 100644 index 0000000000..a32480758a --- /dev/null +++ b/.github/workflows/design-system-adoption.yml @@ -0,0 +1,31 @@ +name: Design system adoption + +# Called by ci-gate.yml. Measured, not enforced — for now. +# +# Its own file because it is the seam with the design system: the four-stage gate +# (wiring · canary · adoption ratchet · style) being built on chore/webkit-gate replaces +# the body of this workflow, and eventually the whole file becomes one `uses:` of +# @aziontech/webkit's own reusable workflow. Keeping that churn out of ci-gate.yml means +# the graph stays still while the design system's contract moves. + +on: + workflow_call: + +permissions: + contents: read + +jobs: + design-system-adoption: + name: Design system adoption + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: ./.github/actions/setup + + - name: Adoption report + if: always() + continue-on-error: true + run: | + pnpm lint:webkit + pnpm --silent report:webkit-adoption >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/internal-links.yml b/.github/workflows/internal-links.yml new file mode 100644 index 0000000000..928c1c5f0e --- /dev/null +++ b/.github/workflows/internal-links.yml @@ -0,0 +1,45 @@ +name: Internal links + +# Called by ci-gate.yml. It runs against the `dist` the build already produced instead of +# building a second time: the checker reads only the sitemaps and the HTML, which is why +# the build packs exactly those. Downloading and unpacking costs 8 s; a second build +# would cost 167 s. +# +# Kept in its own file because it is a self-contained check with its own baseline and its +# own failure mode, and because the `on: workflow_call` shape is what lets it move — to +# another caller, or out of the gate — without touching the graph. + +on: + workflow_call: + inputs: + artifact: + description: Name of the artifact holding the packed HTML and sitemaps. + required: false + type: string + default: dist-html + +permissions: + contents: read + +jobs: + internal-links: + name: Internal links + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: ./.github/actions/setup + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ inputs.artifact }} + + - name: Unpack the build output + run: tar -xzf dist-html.tar.gz + + # Ratchet. The checker had been pointed at the wrong base URL since the Astro fork, + # so it parsed zero pages and always said "no link issues". With the URL fixed it + # reports real numbers, and most of them are links to www.azion.com pages that live + # in the site repo, not here — which is a configuration job, not this branch's. + - name: Link check (ratchet) + run: pnpm ci:ratchet linkcheck From e29722937c04079b6f787e2ec482e248444a541e Mon Sep 17 00:00:00 2001 From: Isaque Santos Date: Thu, 10 Sep 2026 16:48:04 -0300 Subject: [PATCH 07/13] chore(ci): make internal links and design-system adoption standalone workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both were `on: workflow_call`, called from `ci-gate.yml`. They now have their own `on: pull_request` trigger, their own concurrency group and their own status check, and the gate no longer knows about them. Three consequences, none of them free: - Branch protection now needs three names, not one: `CI gate`, `Internal links` and `Design system adoption`. The aggregator still covers everything inside ci-gate.yml, so adding or switching off a job in there is still a one-file change — but these two are outside it now. - Internal links builds the site itself. Artifacts do not cross runs, and a workflow with its own trigger runs in its own run, so the `dist` the gate produces is out of reach. That is ~2 min of runner against the 9 s the download and unpack used to cost. It runs in parallel with the gate, so wall-clock is unchanged. OG image generation is skipped — the checker reads HTML and sitemaps, never the images. - Nothing consumes the artifact any more, so the build stops packing and uploading it. That is 18 s per run of pure waste removed. It comes back the day a job inside the gate needs it. What independence buys: each can be re-run on its own, neither can be held up by an unrelated job, and the design-system one can be replaced wholesale — by the four-stage gate on chore/webkit-gate, and later by a `uses:` of @aziontech/webkit's own reusable workflow — without editing the gate. --- .github/workflows/ci-gate.yml | 54 +++----------------- .github/workflows/design-system-adoption.yml | 33 +++++++++--- .github/workflows/internal-links.yml | 47 +++++++++-------- 3 files changed, 60 insertions(+), 74 deletions(-) diff --git a/.github/workflows/ci-gate.yml b/.github/workflows/ci-gate.yml index 5f4412f2d6..5a9a7e4555 100644 --- a/.github/workflows/ci-gate.yml +++ b/.github/workflows/ci-gate.yml @@ -3,13 +3,14 @@ name: CI gate # The PR gate, in the shape of aziontech/webkit's governance.yml: a `changes` job with path # filters, every job conditioned on one of its outputs, and one aggregator at the end. # -# `CI gate` is the one check to mark required. It passes when every job succeeded or was -# cleanly skipped, so a job can be added or switched off without touching branch protection. +# `CI gate` aggregates the jobs in this file: it passes when every one succeeded or was +# cleanly skipped, so a job can be added or switched off without touching branch +# protection. # -# Two checks live in their own files and are called from here: `internal-links.yml` and -# `design-system-adoption.yml`. Both are `on: workflow_call`, so they run inside this run -# — which is what lets Internal links download the artifact the build produced — while -# staying movable on their own. +# Two checks are deliberately NOT in here — `internal-links.yml` and +# `design-system-adoption.yml` are standalone workflows with their own triggers and their +# own status checks, so branch protection needs all three names. Internal links pays for +# that independence by building the site itself; see the note in its own file. # # This workflow never deploys. Deploys live in their own workflows, with their own secrets # and the opposite concurrency (a deploy is never cancelled mid-flight). @@ -180,11 +181,6 @@ jobs: - name: astro check (ratchet) run: pnpm ci:ratchet astro-check - design-system-adoption: - needs: changes - if: needs.changes.outputs.platform == 'true' - uses: ./.github/workflows/design-system-adoption.yml - build: name: Build needs: changes @@ -202,42 +198,10 @@ jobs: env: NODE_OPTIONS: --max-old-space-size=8120 - # The link checker reads only the sitemaps and the HTML — not the 97 MB of assets. - # 730 MB of HTML packs down to ~94 MB, which is cheaper than a second five-minute - # build, and it is what a11y will consume next. - - name: Pack the HTML for the jobs that consume it - run: | - find dist \( -name '*.html' -o -name 'sitemap-*.xml' \) -type f > dist-files.txt - tar -czf dist-html.tar.gz -T dist-files.txt - ls -lh dist-html.tar.gz - - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: dist-html - path: dist-html.tar.gz - retention-days: 1 - compression-level: 0 - - internal-links: - needs: [changes, build] - # The fan-out guard: with the build red there is nothing to check, so skip cleanly - # instead of burning a runner. - if: ${{ !cancelled() && !contains(needs.*.result, 'failure') }} - uses: ./.github/workflows/internal-links.yml ci-gate: name: CI gate - needs: - [ - changes, - security, - content, - lint, - types, - design-system-adoption, - build, - internal-links - ] + needs: [changes, security, content, lint, types, build] if: always() runs-on: ubuntu-latest timeout-minutes: 5 @@ -252,9 +216,7 @@ jobs: [content]="${{ needs.content.result }}" [lint]="${{ needs.lint.result }}" [types]="${{ needs.types.result }}" - [design-system-adoption]="${{ needs['design-system-adoption'].result }}" [build]="${{ needs.build.result }}" - [internal-links]="${{ needs['internal-links'].result }}" ) failed=0 for job in "${!results[@]}"; do diff --git a/.github/workflows/design-system-adoption.yml b/.github/workflows/design-system-adoption.yml index a32480758a..63d4b79c61 100644 --- a/.github/workflows/design-system-adoption.yml +++ b/.github/workflows/design-system-adoption.yml @@ -1,19 +1,37 @@ name: Design system adoption -# Called by ci-gate.yml. Measured, not enforced — for now. +# Standalone: its own trigger, its own concurrency, its own check. # -# Its own file because it is the seam with the design system: the four-stage gate -# (wiring · canary · adoption ratchet · style) being built on chore/webkit-gate replaces -# the body of this workflow, and eventually the whole file becomes one `uses:` of -# @aziontech/webkit's own reusable workflow. Keeping that churn out of ci-gate.yml means -# the graph stays still while the design system's contract moves. +# It needs nothing from the rest of the pipeline — no build, no artifact — so being +# independent costs it nothing. This is also the seam with the design system: the +# four-stage gate (wiring · canary · adoption ratchet · style) being built on +# chore/webkit-gate replaces the body of this workflow, and eventually the whole file +# becomes one `uses:` of @aziontech/webkit's own reusable workflow. on: - workflow_call: + pull_request: + branches: + - main + - release/new-azion-docs + types: [opened, synchronize, reopened] + paths: + - 'src/**' + - 'plugins/**' + - 'integrations/**' + - 'eslint.config.mjs' + - '.stylelintrc*' + - 'package.json' + - 'pnpm-lock.yaml' + - '.github/workflows/design-system-adoption.yml' + - '.github/actions/setup/**' permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + jobs: design-system-adoption: name: Design system adoption @@ -23,6 +41,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/setup + # Measured, not enforced — for now. - name: Adoption report if: always() continue-on-error: true diff --git a/.github/workflows/internal-links.yml b/.github/workflows/internal-links.yml index 928c1c5f0e..ab82c7934d 100644 --- a/.github/workflows/internal-links.yml +++ b/.github/workflows/internal-links.yml @@ -1,41 +1,46 @@ name: Internal links -# Called by ci-gate.yml. It runs against the `dist` the build already produced instead of -# building a second time: the checker reads only the sitemaps and the HTML, which is why -# the build packs exactly those. Downloading and unpacking costs 8 s; a second build -# would cost 167 s. +# Standalone: its own trigger, its own concurrency, its own check. # -# Kept in its own file because it is a self-contained check with its own baseline and its -# own failure mode, and because the `on: workflow_call` shape is what lets it move — to -# another caller, or out of the gate — without touching the graph. +# Being independent has a price, and it is worth naming. This workflow builds the site +# itself — ~2 min — because a workflow with its own trigger runs in its own run, and +# artifacts do not cross runs. While it was called from `ci-gate.yml` it downloaded the +# `dist` the gate had already produced and cost 9 s instead. +# +# What independence buys: it can be re-run on its own, it cannot be held up by an +# unrelated job, and it can be moved or switched off without editing the gate. It runs in +# parallel with the gate, so the extra build costs runner minutes, not wall-clock. +# +# OG image generation is skipped: the checker reads HTML and sitemaps, never the images. on: - workflow_call: - inputs: - artifact: - description: Name of the artifact holding the packed HTML and sitemaps. - required: false - type: string - default: dist-html + pull_request: + branches: + - main + - release/new-azion-docs + types: [opened, synchronize, reopened] permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + jobs: internal-links: name: Internal links runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 25 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/setup - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ inputs.artifact }} - - - name: Unpack the build output - run: tar -xzf dist-html.tar.gz + - name: Build the site + run: pnpm build:local + env: + NODE_OPTIONS: --max-old-space-size=8120 + SKIP_OG: 'true' # Ratchet. The checker had been pointed at the wrong base URL since the Astro fork, # so it parsed zero pages and always said "no link issues". With the URL fixed it From 65572a3878819e3d73fae4e57eb7ceb5b7f215b4 Mon Sep 17 00:00:00 2001 From: Isaque Santos Date: Thu, 10 Sep 2026 17:44:43 -0300 Subject: [PATCH 08/13] docs(governance): describe the CI that exists, and finish the timeout pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two loose ends from the same source, both belonging to work Marcus did in #2307 and #2313. GOVERNANCE.md still said the CI checks the PR title and that a weekly link check opens an issue. This branch deleted both workflows, so both sentences were false — and by the document's own rule ("if another document in this repo contradicts this one, this one wins and the other gets a PR"), fixing that belongs here rather than in a follow-up. The section now describes the three required checks, what blocks, what ratchets and with which numbers, and what only reports. And `timeout-minutes` reaches the three deploy workflows. It was Marcus's discipline on the PR check from the start; the earlier pass in this branch gave them `concurrency` and `permissions` and forgot the timeout, leaving them on GitHub's six-hour default. Verified: every job in every workflow now declares a timeout, and no sentence in GOVERNANCE.md describes a check that does not exist. --- .github/GOVERNANCE.md | 47 +++++++++++++++++++++++++++++++------ .github/workflows/dev.yml | 1 + .github/workflows/prod.yml | 1 + .github/workflows/stage.yml | 1 + 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/.github/GOVERNANCE.md b/.github/GOVERNANCE.md index 0f3c26f2a5..5029d10b10 100644 --- a/.github/GOVERNANCE.md +++ b/.github/GOVERNANCE.md @@ -66,13 +66,46 @@ AI-assisted PRs follow the same rules as any other. The author, not the agent, i ## What CI checks -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. - -Weekly: a link check crawls the built site for broken internal links and opens an issue when it finds them. - -Everything else in this document is a convention that reviewers uphold, which is how most of it will always work. +Three checks have to be green to merge, and each one is its own workflow. + +**CI gate** is the aggregator. A `changes` job reads the diff and decides which of the jobs below +run, so a PR that only touches `.mdx` never pays for the platform checks. The gate passes when +every job it covers either succeeded or was cleanly skipped, which is what lets a job be added or +switched off without editing branch protection. + +- The site builds, and frontmatter namespaces and permalinks are present and unique. A broken + build or a duplicate permalink blocks the merge. +- The navigation tree validates. A broken entry there takes down every page at once, so this one + blocks outright. +- ESLint, Prettier and Stylelint run **over the files the PR changed**. Over the whole tree they + report debt nobody in a given PR created; scoped to the diff they mean what you touch, you leave + clean. +- Dependencies are audited and the diff is scanned for verified secrets. This repository is + public: a token pasted into an example goes into the history and does not come out. + +**Internal links** builds the site and checks every internal link in it. + +**Design system adoption** 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 reports; it never blocks. It exists so the distance between this +codebase and the design system is a number someone can watch, instead of something noticed in +review. + +### Checks that ratchet + +Four checks carry more debt than any one PR can clear, so they are frozen at a baseline in +`ci/baselines.json` and fail only on a number that **grows**: high/critical vulnerabilities (34), +type errors from `astro check` (54), translations whose slug does not match the English page (745), +and broken internal links (21,664 — most of them links to `www.azion.com` pages that live in the +site repository, not here). + +The count can fall and the baseline is then stale, which is reported and never punished; re-snapshot +with `pnpm ci:ratchet --update`. A ratchet whose command stops producing a number **fails** +rather than passing, because a check that silently measures nothing is worse than no check at all — +which is exactly what the link checker did for as long as it existed. + +Everything else in this document is a convention that reviewers uphold, which is how most of it will +always work. ## Review expectations diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 8e18960113..e4f63b6d96 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -15,6 +15,7 @@ jobs: deploy: name: Development publising runs-on: ubuntu-latest + timeout-minutes: 30 steps: - name: CHECKOUT project uses: actions/checkout@v4 diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index c42dfa528c..c4a8a94eba 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -18,6 +18,7 @@ jobs: deploy: name: Production publising runs-on: ubuntu-latest + timeout-minutes: 45 steps: - name: CHECKOUT project uses: actions/checkout@v4 diff --git a/.github/workflows/stage.yml b/.github/workflows/stage.yml index 0b343f1421..7c160b0a8c 100644 --- a/.github/workflows/stage.yml +++ b/.github/workflows/stage.yml @@ -15,6 +15,7 @@ jobs: deploy: name: Development publising runs-on: ubuntu-latest + timeout-minutes: 30 steps: - name: CHECKOUT project uses: actions/checkout@v4 From bb59b564bca258e0cd581bcceaf7193afca473d1 Mon Sep 17 00:00:00 2001 From: Isaque Santos Date: Thu, 10 Sep 2026 17:54:37 -0300 Subject: [PATCH 09/13] chore(ci): make the design-system check fail on new violations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The adoption report ran with `continue-on-error`, so the check was green with 72 violations across 30 files and an adoption score of 65%. A check that is always green while the code is full of violations is worse than no check: it teaches everyone that green means nothing, and it is the same failure mode as the link checker that reported "no link issues" for years because it parsed zero pages. It now ratchets, like the other four. The report still goes to the Summary first, unconditionally — the number reaches the reader whatever the ratchet decides — and then `pnpm ci:ratchet webkit-adoption` fails on a violation the branch ADDS. The 72 already here stay green; fixing one is reported, never punished, and the count can only go down. Baseline read from the report's own Markdown, not from a count on stderr, so the ratchet fails rather than passes if the report stops producing the number. --- .github/workflows/design-system-adoption.yml | 16 +++++++++++++++- ci/baselines.json | 6 ++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/design-system-adoption.yml b/.github/workflows/design-system-adoption.yml index 63d4b79c61..08e5bb1b56 100644 --- a/.github/workflows/design-system-adoption.yml +++ b/.github/workflows/design-system-adoption.yml @@ -2,6 +2,9 @@ name: Design system adoption # Standalone: its own trigger, its own concurrency, its own check. # +# The number is reported to the Summary and then ratcheted: the 72 violations already in +# the tree stay green, a new one fails the check. +# # It needs nothing from the rest of the pipeline — no build, no artifact — so being # independent costs it nothing. This is also the seam with the design system: the # four-stage gate (wiring · canary · adoption ratchet · style) being built on @@ -41,10 +44,21 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/setup - # Measured, not enforced — for now. + # The report goes to the Summary first, unconditionally, so the number reaches the + # reader whatever the ratchet below decides. - name: Adoption report if: always() continue-on-error: true run: | pnpm lint:webkit pnpm --silent report:webkit-adoption >> "$GITHUB_STEP_SUMMARY" + + # Blocking. 72 violations across 30 files are already here, so this fails on a + # violation the branch ADDS, never on the count it inherits. A report that is always + # green while the code is full of violations is worse than no check: it teaches + # everyone that green means nothing. + # + # Fixing one is reported, never punished — re-snapshot with + # `pnpm ci:ratchet webkit-adoption --update`. The count can only go down. + - name: No new violations (ratchet) + run: pnpm ci:ratchet webkit-adoption diff --git a/ci/baselines.json b/ci/baselines.json index f1d4fc11cc..bdc190a65a 100644 --- a/ci/baselines.json +++ b/ci/baselines.json @@ -22,5 +22,11 @@ "pattern": "Found (?:(\\d+)|no) link issues", "baseline": 21664, "label": "links quebrados no build" + }, + "webkit-adoption": { + "command": "pnpm -s lint:webkit && pnpm -s report:webkit-adoption", + "pattern": "\\| Violations \\| \\*\\*(\\d+)\\*\\* \\|", + "baseline": 72, + "label": "violações das regras do design system" } } From 9e98f0af57cc7d536edf4f01cdfbe999f1844a99 Mon Sep 17 00:00:00 2001 From: Isaque Santos Date: Thu, 10 Sep 2026 18:01:56 -0300 Subject: [PATCH 10/13] chore(ci): stop tolerating design-system violations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ratchet was set at 72, so the check stayed green over the 72 violations already in the tree — which is not what was asked for. The baseline is now zero: no violation is tolerated. This check is red as of this commit and stays red until the 72 violations across 30 files are fixed. That is deliberate. Two rounds ago the step ran with `continue-on-error` and reported green over all of them; a ratchet at 72 kept the merge unblocked, which is the softer version of the same problem. The report still goes to the Summary first, so the number and the by-rule and by-file tables are there for whoever picks the work up: 26 no-style-override, 25 authoring-standards, 16 no-hardcoded-color, 2 no-hardcoded-motion, 2 prefer-tree-shakeable-root, 1 valid-import-path. The four other ratchets keep their inherited baselines — audit 34, astro check 54, slugs 745, links 21664 — and `scripts/ci/ratchet.mjs` now words a zero-baseline failure as "none are tolerated" instead of claiming the branch added all of them. --- .github/workflows/design-system-adoption.yml | 18 +++++++++--------- ci/baselines.json | 2 +- scripts/ci/ratchet.mjs | 10 +++++++--- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/.github/workflows/design-system-adoption.yml b/.github/workflows/design-system-adoption.yml index 08e5bb1b56..4ea3ca0505 100644 --- a/.github/workflows/design-system-adoption.yml +++ b/.github/workflows/design-system-adoption.yml @@ -2,8 +2,8 @@ name: Design system adoption # Standalone: its own trigger, its own concurrency, its own check. # -# The number is reported to the Summary and then ratcheted: the 72 violations already in -# the tree stay green, a new one fails the check. +# The number is reported to the Summary and then enforced with no tolerance: the baseline +# is zero, so the 72 violations currently in the tree fail this check until they are fixed. # # It needs nothing from the rest of the pipeline — no build, no artifact — so being # independent costs it nothing. This is also the seam with the design system: the @@ -53,12 +53,12 @@ jobs: pnpm lint:webkit pnpm --silent report:webkit-adoption >> "$GITHUB_STEP_SUMMARY" - # Blocking. 72 violations across 30 files are already here, so this fails on a - # violation the branch ADDS, never on the count it inherits. A report that is always - # green while the code is full of violations is worse than no check: it teaches - # everyone that green means nothing. + # Blocking, with the baseline at zero: no violation is tolerated. This is red today — + # 72 violations across 30 files, adoption 65% — and stays red until they are fixed. # - # Fixing one is reported, never punished — re-snapshot with - # `pnpm ci:ratchet webkit-adoption --update`. The count can only go down. - - name: No new violations (ratchet) + # That is the point. The step used to run with `continue-on-error` and reported green + # over all 72, which is the same failure mode as the link checker that said "no link + # issues" because it parsed zero pages: a check nobody can trust. A ratchet at 72 + # would have kept the merge unblocked, and was rejected for the same reason. + - name: No design-system violations run: pnpm ci:ratchet webkit-adoption diff --git a/ci/baselines.json b/ci/baselines.json index bdc190a65a..49a05bd9a4 100644 --- a/ci/baselines.json +++ b/ci/baselines.json @@ -26,7 +26,7 @@ "webkit-adoption": { "command": "pnpm -s lint:webkit && pnpm -s report:webkit-adoption", "pattern": "\\| Violations \\| \\*\\*(\\d+)\\*\\* \\|", - "baseline": 72, + "baseline": 0, "label": "violações das regras do design system" } } diff --git a/scripts/ci/ratchet.mjs b/scripts/ci/ratchet.mjs index 32eff59e38..f38ce94458 100644 --- a/scripts/ci/ratchet.mjs +++ b/scripts/ci/ratchet.mjs @@ -65,10 +65,14 @@ function check(name, entry, update) { } if (count > entry.baseline) { + // A baseline of zero is not frozen debt, it is "none tolerated" — saying the branch + // "adds" all of them would be a lie. console.error( - `FAIL ${name}: ${count} ${entry.label} — the baseline is ${ - entry.baseline - }, so this branch adds ${count - entry.baseline}.` + entry.baseline === 0 + ? `FAIL ${name}: ${count} ${entry.label}. The baseline is zero — none are tolerated.` + : `FAIL ${name}: ${count} ${entry.label} — the baseline is ${ + entry.baseline + }, so this branch adds ${count - entry.baseline}.` ); console.error(output.split('\n').slice(-40).join('\n')); summary(`| ${name} | **${count}** | ${entry.baseline} | +${count - entry.baseline} |`); From 2c8faef16a0ba9f7cb9a4b9a4595d56c9efc9eed Mon Sep 17 00:00:00 2001 From: Isaque Santos Date: Fri, 11 Sep 2026 15:30:57 -0300 Subject: [PATCH 11/13] chore(ci): re-snapshot on the new base, and make three silent passes fail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up, plus the class of defect the review names: a check that measures nothing must fail, not pass. Rebased onto f048e07e2. That rebuild of the template on webkit 5 moved every baseline, so they are re-snapshotted: audit 34 → 32, astro check 54 → 32, slugs 745 → 744, and design-system violations 72 → 0. The design-system check keeps its zero baseline and is now satisfiable — 86 of 86 UI files clean. Three ways a check could have measured nothing and reported success: - The link checker prints "Found no link issues. Great job!" when it parsed zero pages, and the ratchet read that as zero. That is the exact bug this PR was opened to fix, reachable again through a missing build or a base URL that matches no . It now throws instead, naming both causes. - The adoption report scored 100% when it saw no UI files at all — a stale or empty ESLint report, or a crash swallowed by the `|| true` in `lint:webkit`. It now throws. - `count > undefined` is false, so a missing or mistyped baseline in `ci/baselines.json` silently disabled its check. The ratchet now rejects a baseline that is not a number. The first guard earned its place immediately: the local build cannot resolve the pricing host, so it produced a `dist` with no sitemap, and the link check said so instead of reporting a clean run. Also from the review: `design-system-adoption.yml` loses its `paths:` filter. A required check whose path filter does not match never runs, never reports, and leaves branch protection waiting on it forever — the 36 s it costs on every PR is the cheaper half of that trade. And GOVERNANCE.md said the design-system check "reports; it never blocks" while the workflow blocked; it now describes what runs, and stops copying baseline numbers into prose where they go stale. The `linkcheck` baseline is deliberately left stale in this commit: the build needs network this machine does not have, so the honest number comes from CI. --- .github/GOVERNANCE.md | 19 +++++----- .github/workflows/design-system-adoption.yml | 14 +++----- ci/baselines.json | 6 ++-- scripts/ci/ratchet.mjs | 7 ++++ scripts/lint-linkcheck.ts | 10 ++++++ scripts/webkit-adoption-report.mjs | 38 +++++++++++++------- 6 files changed, 60 insertions(+), 34 deletions(-) diff --git a/.github/GOVERNANCE.md b/.github/GOVERNANCE.md index 5029d10b10..677f2e0b6a 100644 --- a/.github/GOVERNANCE.md +++ b/.github/GOVERNANCE.md @@ -85,19 +85,22 @@ switched off without editing branch protection. **Internal links** builds the site and checks every internal link in it. -**Design system adoption** runs the design-system ESLint rules over the UI and writes the result to +**Design system adoption** runs the design-system ESLint rules over the UI, 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 reports; it never blocks. It exists so the distance between this -codebase and the design system is a number someone can watch, instead of something noticed in -review. +check did *not* look at — and then fails on any violation at all. The report comes first and always, +so the number reaches you whatever the check decides; the check itself tolerates nothing. ### Checks that ratchet Four checks carry more debt than any one PR can clear, so they are frozen at a baseline in -`ci/baselines.json` and fail only on a number that **grows**: high/critical vulnerabilities (34), -type errors from `astro check` (54), translations whose slug does not match the English page (745), -and broken internal links (21,664 — most of them links to `www.azion.com` pages that live in the -site repository, not here). +`ci/baselines.json` and fail only on a number that **grows**: high/critical vulnerabilities, type +errors from `astro check`, translations whose slug does not match the English page, and broken +internal links (most of those are links to `www.azion.com` pages that live in the site repository, +not here). The current numbers are in `ci/baselines.json`; this document does not repeat them, +because a number copied into prose goes stale the first time someone fixes something. + +Design-system adoption is deliberately **not** one of them: its baseline is zero, so every +violation fails the check. The count can fall and the baseline is then stale, which is reported and never punished; re-snapshot with `pnpm ci:ratchet --update`. A ratchet whose command stops producing a number **fails** diff --git a/.github/workflows/design-system-adoption.yml b/.github/workflows/design-system-adoption.yml index 4ea3ca0505..143f5eeb79 100644 --- a/.github/workflows/design-system-adoption.yml +++ b/.github/workflows/design-system-adoption.yml @@ -17,16 +17,10 @@ on: - main - release/new-azion-docs types: [opened, synchronize, reopened] - paths: - - 'src/**' - - 'plugins/**' - - 'integrations/**' - - 'eslint.config.mjs' - - '.stylelintrc*' - - 'package.json' - - 'pnpm-lock.yaml' - - '.github/workflows/design-system-adoption.yml' - - '.github/actions/setup/**' + # Deliberately no `paths:` filter. This is a required check, and a workflow whose path + # filter does not match never runs — so it never reports, and branch protection waits + # on it forever. A check that is cheap (36 s) and always reports beats a check that is + # sometimes free and sometimes deadlocks the merge. permissions: contents: read diff --git a/ci/baselines.json b/ci/baselines.json index 49a05bd9a4..64940fe84b 100644 --- a/ci/baselines.json +++ b/ci/baselines.json @@ -2,19 +2,19 @@ "audit": { "command": "node scripts/ci/audit-count.mjs", "pattern": "high\\+critical: (\\d+)", - "baseline": 34, + "baseline": 32, "label": "vulnerabilidades high/critical nas dependências" }, "astro-check": { "command": "pnpm -s check", "pattern": "-\\s+(\\d+)\\s+errors", - "baseline": 54, + "baseline": 32, "label": "erros de tipo" }, "slugcheck": { "command": "pnpm -s lint:slugcheck", "pattern": "Found (\\d+) translations with mismatched slugs", - "baseline": 745, + "baseline": 744, "label": "traduções com slug divergente do inglês" }, "linkcheck": { diff --git a/scripts/ci/ratchet.mjs b/scripts/ci/ratchet.mjs index f38ce94458..04379072ca 100644 --- a/scripts/ci/ratchet.mjs +++ b/scripts/ci/ratchet.mjs @@ -56,6 +56,13 @@ function check(name, entry, update) { } // A pattern may match a "none found" phrasing with no digits — that is a real zero. + // `count > undefined` is false, so a missing or mistyped baseline would silently + // disable the check instead of failing it. + if (typeof entry.baseline !== 'number' || !Number.isFinite(entry.baseline)) { + console.error(`FAIL ${name}: baseline is not a number (${JSON.stringify(entry.baseline)}).`); + return false; + } + const count = match[1] === undefined ? 0 : Number(match[1]); if (update) { diff --git a/scripts/lint-linkcheck.ts b/scripts/lint-linkcheck.ts index cb2b0e6b30..a9762fb2cd 100644 --- a/scripts/lint-linkcheck.ts +++ b/scripts/lint-linkcheck.ts @@ -28,6 +28,16 @@ class LinkChecker { const pagePathnames = getPagePathnamesFromSitemap(options); + // A checker that parsed no pages is broken, not clean — and it would print + // "Found no link issues. Great job!", which is exactly how this tool spent its whole + // life green while matching a base URL that appears in no sitemap. + if (pagePathnames.length === 0) { + throw new Error( + `No pages found in the sitemaps under ${options.buildOutputDir} for base URL ${options.baseUrl}. ` + + 'Either the build output is missing, or the base URL does not match the entries.' + ); + } + const allPages = parsePages(pagePathnames, options); const linkIssues = findLinkIssues(allPages, options, state); diff --git a/scripts/webkit-adoption-report.mjs b/scripts/webkit-adoption-report.mjs index 49a333e42d..3706fc64cf 100644 --- a/scripts/webkit-adoption-report.mjs +++ b/scripts/webkit-adoption-report.mjs @@ -41,7 +41,7 @@ function parseArgs(argv) { const format = formatIndex === -1 ? 'markdown' : args[formatIndex + 1]; if (!file) { process.stderr.write( - 'usage: node scripts/webkit-adoption-report.mjs [--format markdown|json]\n', + 'usage: node scripts/webkit-adoption-report.mjs [--format markdown|json]\n' ); process.exit(1); } @@ -95,11 +95,20 @@ function collect(results, cwd) { } } + // Seeing no UI files at all means the lint never reached the components — a stale or + // empty report, a changed layout, a crashed ESLint swallowed by `|| true`. Reporting + // "100%, zero violations" from that is the same lie the link checker used to tell. + if (uiFiles.size === 0) { + throw new Error( + 'No UI files appear in the ESLint report. The lint did not reach the components; ' + + 're-run `pnpm lint:webkit` and check that it produced findings.' + ); + } + const dirtyUiFiles = [...byFile.keys()].filter((f) => UI_EXTENSIONS.has(extensionOf(f))); // Same shape as the architecture report in azion-console-kit: share of files that are // clean, not of violations. One file with 20 findings weighs the same as one with 1. - const score = - uiFiles.size === 0 ? 100 : Math.round((1 - dirtyUiFiles.length / uiFiles.size) * 100); + const score = Math.round((1 - dirtyUiFiles.length / uiFiles.size) * 100); return { total, @@ -133,7 +142,7 @@ function renderMarkdown(report, catalog) { push( '> **The webkit catalog could not be resolved, so 8 of the 12 rules silently did nothing.** ' + 'This report is not a clean bill of health — install `@aziontech/webkit` or set ' + - '`WEBKIT_CATALOG_PATH`, then run it again.', + '`WEBKIT_CATALOG_PATH`, then run it again.' ); push(); } @@ -146,7 +155,9 @@ function renderMarkdown(report, catalog) { push(`| Violations | **${report.total}** |`); push(`| Files affected | ${report.filesAffected} |`); push( - `| UI files clean | ${report.uiFilesClean} of ${report.uiFilesTotal} — **${report.score}%** (${statusFor(report.score)}) |`, + `| UI files clean | ${report.uiFilesClean} of ${report.uiFilesTotal} — **${ + report.score + }%** (${statusFor(report.score)}) |` ); push(); @@ -186,18 +197,19 @@ function renderMarkdown(report, catalog) { : 'none'; push(`- Violations found in: ${extensions}.`); push( - `- The adoption score counts ${[...UI_EXTENSIONS].map((e) => `\`.${e}\``).join(' and ')} files only — ` + - 'those are the ones the design system governs.', + `- The adoption score counts ${[...UI_EXTENSIONS] + .map((e) => `\`.${e}\``) + .join(' and ')} files only — ` + 'those are the ones the design system governs.' ); push( - '- `no-style-override` does **not** run on `.astro`: it needs vue-eslint-parser\'s template ' + + "- `no-style-override` does **not** run on `.astro`: it needs vue-eslint-parser's template " + 'visitor, which astro-eslint-parser does not provide. Restyled webkit components inside ' + - 'Astro files are invisible here.', + 'Astro files are invisible here.' ); push( '- Raw HTML where a webkit component exists (`