Skip to content

Site: take sharp 0.35.5 (GHSA-wq5f-xc86-pv6w) #816

Site: take sharp 0.35.5 (GHSA-wq5f-xc86-pv6w)

Site: take sharp 0.35.5 (GHSA-wq5f-xc86-pv6w) #816

Workflow file for this run

name: Site
on:
push:
branches: [main]
# Not just site/. The build reads four files from outside it: CHANGELOG.md
# (changelog.astro imports it with ?raw), scripts/install.ps1 and
# scripts/install.sh (served at /install.ps1 and /install.sh), and
# apps/netscli-gui/package.json (site-content/version.ts reads the version
# from it). Keep this list, the `changes` job's grep below, and
# site-preview.yml's two filters in step -- they are the same list three
# times and a media query cannot read a variable here either.
paths:
- 'site/**'
- 'CHANGELOG.md'
- 'scripts/install.ps1'
- 'scripts/install.sh'
- 'apps/netscli-gui/package.json'
# NOTE: deliberately unfiltered, unlike the push trigger above. `Site
# Gate` below is a required status check, and a required check that
# never *reports* leaves a PR stuck on "Expected — waiting for status"
# forever. A `paths:` filter here would do exactly that for every PR
# that does not touch site/. So the workflow always runs and the real
# jobs skip themselves via `changes`. A skipped job satisfies a
# required check; an absent one does not.
pull_request:
branches: [main]
workflow_dispatch:
concurrency:
group: site-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
changes:
name: Detect site changes
runs-on: ubuntu-latest
outputs:
site: ${{ steps.filter.outputs.site }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- id: filter
shell: bash
# Through `env:` rather than straight into the script — see the
# equivalent note in ci.yml's `changes` job.
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
if [[ "${{ github.event_name }}" != "pull_request" ]]; then
echo "site=true" >> "$GITHUB_OUTPUT"
exit 0
fi
files=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")
echo "Changed files:"
echo "$files" | sed 's/^/ /'
# Same set as the push trigger's paths list above.
if echo "$files" | grep -qE '^(site/|CHANGELOG\.md$|scripts/install\.(ps1|sh)$|apps/netscli-gui/package\.json$)'; then
echo "site=true" >> "$GITHUB_OUTPUT"
else
echo "site=false" >> "$GITHUB_OUTPUT"
echo "No site/ changes — site jobs will skip."
fi
maintainability:
name: File size guard
needs: changes
if: needs.changes.outputs.site == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .nvmrc
- run: node scripts/check-file-size.mjs
# Also runs in ci.yml, deliberately. That job is gated on `code`, which a
# site-only pull request sets to false, so without this the site's own
# contrast could regress with nothing looking at it. The script checks
# both surfaces and takes milliseconds, so running it twice costs nothing
# and neither workflow can be the one that forgot.
- name: Design token contrast
run: node scripts/design-tokens.mjs
# Deleting or renaming a docs page can strand a link inside the desktop
# app, which no site check would otherwise notice.
- name: App doc links
run: node scripts/check-app-doc-links.mjs
# A ratchet on dead CSS, not a target. `css-shadowing.mjs` proves a
# declaration can never affect the page -- same selector, same media
# context, a later rule of equal-or-greater importance. It was written
# for B-23, run once in the pull request that added it, and then never
# again, so the count it had driven down grew back to 210 unnoticed.
# Lower the budget when you prune; it must never rise.
- name: Dead CSS budget
working-directory: site
run: npm run check:css
- name: CSS region guard
working-directory: site
run: npm run check:regions
# Reads the wordmark PNG and checks the token that cancels its
# transparent left margin still matches it. Needs no build and no
# browser: it is here rather than in the build job so a bad re-export
# fails in seconds. See the script for the five drifted values that
# made it worth checking.
- name: Wordmark inset
working-directory: site
run: npm run check:wordmark
build:
name: Build + typecheck
needs: [changes, maintainability]
if: needs.changes.outputs.site == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: site
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Tags, not just the tip. `check:changelog` asks git which versions
# are actually tagged so it can catch a changelog entry dated before
# its release exists -- and with the default `fetch-depth: 1` there
# are no tags to ask about, so it would report that it could not
# check rather than passing on nothing.
fetch-depth: 0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: site/package-lock.json
- run: npm ci
- run: npm run build
- run: npm run check
# Reads the built changelog page and compares it against CHANGELOG.md.
# Both halves of this went wrong in one day and neither was visible in
# `astro check` or the a11y pass -- see the header of the script.
- name: Changelog dates
run: npm run check:changelog
# JSON-LD renders as nothing, so a malformed breadcrumb shipped on every
# docs page until Search Console flagged it. See the script's header.
- name: Structured data
run: npm run check:structured-data
# Runs axe against both themes; see site/scripts/a11y.mjs for why
# both are needed, and why it prefers the runner's matched
# chromedriver over the one the npm package downloads.
- name: Accessibility check
run: npm run test:a11y
# Runs alongside axe rather than instead of it, because it covers the
# one thing axe cannot: axe files text over a gradient or a
# pseudo-element under `incomplete`, and `--exit` only fails on
# `violations`. On the landing page that was 48 unchecked nodes reported
# as a clean pass -- including a heading at 1.03:1. This composites the
# ancestor stack itself. See the header of the script.
- name: Rendered contrast sweep
run: npm run check:contrast
# Where the docs header's controls sit, at seventeen widths.
#
# Two failures shipped that nothing above could see, because both were
# about position rather than markup, colour or speed: the search control
# stranded beside the wordmark with up to 861px of empty bar beside it
# from 901px to 1152px, and no theme control reachable anywhere between
# 800px and 1152px. Both came from a breakpoint moving in one file while
# the rule depending on it stayed put in another. See the header of the
# script.
- name: Docs header controls
run: npm run check:header
# Lighthouse across every built route, with a floor per category.
#
# Catches the class of regression the checks above cannot see at all: a
# hero image shipped without dimensions, a render-blocking script, a
# heading level skipped inside generated markup. The last of those was
# live on the changelog page and no existing gate reported it -- axe
# passes it, and the contrast sweep only reads colour.
#
# The performance floor sits at 85 rather than near the 100 the site
# scores, because it is the one number here that moves with how busy the
# machine is: the same three pages measured 91-93 under load and 100
# idle, on identical code. See the floors in the script for the numbers.
- name: Lighthouse
run: npm run check:lighthouse
# ─── Required status check ────────────────────────────────────────────
# The only job here that should be marked required in branch protection.
# See the equivalent note in ci.yml.
site-gate:
name: Site Gate
if: always()
needs:
- changes
- maintainability
- build
runs-on: ubuntu-latest
steps:
- name: Verify no upstream job failed
shell: bash
env:
NEEDS: ${{ toJSON(needs) }}
run: |
set -euo pipefail
echo "$NEEDS" | jq -r 'to_entries[] | " \(.key): \(.value.result)"'
# Allowlist success/skipped rather than denylisting failure/
# cancelled — see the equivalent note in ci.yml. A job whose runner
# never starts reports "abandoned", which matched neither arm and
# let the required check pass over a failed job.
bad=$(echo "$NEEDS" | jq -r '
to_entries
| map(select(.value.result != "success" and .value.result != "skipped"))
| map("\(.key)=\(.value.result)")
| join(", ")
')
if [[ -n "$bad" ]]; then
echo "::error::Site Gate failed — these jobs did not succeed: ${bad}"
exit 1
fi
echo "Site Gate passed (all jobs succeeded or were legitimately skipped)."