Repository navigation
JavaScript port: desktop scrollbars for mouse-driven browsers #9356
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Blog Prose Checks | |
| # Diff-scoped, net-new prose gate for the Hugo blog, plus a non-blocking | |
| # weekly whole-corpus report. This is the blog counterpart of | |
| # "Build Developer Guide Docs", but unlike the guide it does NOT gate the whole | |
| # corpus: the blog is 854 mostly-historical posts, so the gate fails only on | |
| # findings a PR INTRODUCES (scripts/website/blog_prose_gate.py diffs each | |
| # changed post against its merge-base version). The backlog stays grandfathered; | |
| # the weekly report job tracks it without blocking anyone. | |
| on: | |
| # Runs on EVERY pull request (no path filter) so the gate can be registered as | |
| # a required status check on the base branch without the "required check never | |
| # reports" deadlock that path-filtered workflows cause. The job itself detects | |
| # whether any blog content changed and fast-skips (passing) when none did, so | |
| # non-blog PRs stay cheap. | |
| pull_request: | |
| schedule: | |
| # Weekly whole-corpus backlog report (Mondays 06:00 UTC). Non-blocking. | |
| - cron: '0 6 * * 1' | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| env: | |
| VALE_VERSION: '3.13.0' | |
| concurrency: | |
| # Only the latest revision needs validation. PR numbers keep forks isolated; | |
| # event names keep a manual/scheduled run from cancelling a push or PR run. | |
| group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # --------------------------------------------------------------------------- # | |
| # PR gate — fails only on net-new findings in changed posts. | |
| # --------------------------------------------------------------------------- # | |
| gate: | |
| name: Blog prose gate | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v6 | |
| with: | |
| # Full history so the gate can compute the merge-base and read the | |
| # base version of each changed post. | |
| fetch-depth: 0 | |
| - name: Detect changed prose and diagram rendering inputs | |
| id: detect | |
| run: | | |
| set -euo pipefail | |
| base="origin/${GITHUB_BASE_REF}" | |
| mb="$(git merge-base "$base" HEAD 2>/dev/null || true)" | |
| if [ -z "$mb" ]; then | |
| count=0 | |
| else | |
| # Deleting a gate input must still run validation and expose the missing file. | |
| count="$(git diff --name-only --diff-filter=ACDMR "$mb" HEAD -- 'docs/website/content/blog/*.md' 'docs/website/social/linkedin/*.md' 'docs/website/layouts/shortcodes/mermaid.html' 'docs/website/hugo.toml' 'scripts/website/validate_mermaid.mjs' 'scripts/website/test_validate_mermaid.mjs' '.github/workflows/blog-prose.yml' | wc -l | tr -d ' ')" | |
| fi | |
| echo "Changed blog and social files: $count" | |
| if [ "$count" -gt 0 ]; then | |
| echo "has_posts=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "has_posts=false" >> "$GITHUB_OUTPUT" | |
| echo "No blog content changed — prose gate skips its heavy steps and passes." | |
| fi | |
| - name: Set up Python 3 | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.12' | |
| - name: Test blog prose gate | |
| run: python3 scripts/website/test_blog_prose_gate.py | |
| - name: Validate LinkedIn queue inputs | |
| if: steps.detect.outputs.has_posts == 'true' | |
| run: | | |
| python3 scripts/website/test_queue_social_posts.py | |
| python3 scripts/website/queue_social_posts.py --validate-only | |
| - name: Install python-markdown | |
| if: steps.detect.outputs.has_posts == 'true' | |
| run: pip install --user 'markdown==3.7' | |
| - name: Install Vale | |
| if: steps.detect.outputs.has_posts == 'true' | |
| run: | | |
| set -euo pipefail | |
| ARCHIVE="vale_${VALE_VERSION}_Linux_64-bit.tar.gz" | |
| curl -fsSL -o "$ARCHIVE" "https://github.com/errata-ai/vale/releases/download/v${VALE_VERSION}/${ARCHIVE}" | |
| tar -xzf "$ARCHIVE" | |
| sudo mv vale /usr/local/bin/vale | |
| rm -f "$ARCHIVE" | |
| - name: Sync Vale styles | |
| if: steps.detect.outputs.has_posts == 'true' | |
| run: vale sync --config docs/website/.vale.ini | |
| - name: Set up Java 17 for LanguageTool | |
| if: steps.detect.outputs.has_posts == 'true' | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| java-version: '17' | |
| - name: Install language-tool-python | |
| if: steps.detect.outputs.has_posts == 'true' | |
| run: pip install --user language-tool-python==2.9.4 | |
| - name: Run net-new prose gate | |
| id: gate | |
| if: steps.detect.outputs.has_posts == 'true' | |
| run: | | |
| set -euo pipefail | |
| mkdir -p build/blog-prose | |
| set +e | |
| python3 scripts/website/blog_prose_gate.py \ | |
| --base-ref "origin/${GITHUB_BASE_REF}" \ | |
| --report build/blog-prose/net-new.json | |
| echo "status=$?" >> "$GITHUB_OUTPUT" | |
| set -e | |
| - name: Set up Node 20 for Mermaid validation | |
| if: steps.detect.outputs.has_posts == 'true' | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '20' | |
| - name: Install Playwright browsers | |
| if: steps.detect.outputs.has_posts == 'true' | |
| run: | | |
| set -euo pipefail | |
| cd scripts | |
| npm ci | |
| # Playwright shells out to apt for the OS-level deps, so the runner | |
| # image's third-party sources have to be pruned FIRST -- a broken | |
| # vendor mirror makes its apt-get fail as a whole and it reports | |
| # only "Failed to install browser dependencies". | |
| bash "$GITHUB_WORKSPACE/scripts/ci/apt-get-update.sh" | |
| npx playwright install-deps chromium firefox | |
| npx playwright install chromium firefox | |
| - name: Test Mermaid render gate | |
| if: steps.detect.outputs.has_posts == 'true' | |
| run: node --test scripts/website/test_validate_mermaid.mjs | |
| - name: Set up Hugo for rendered diagram checks | |
| if: steps.detect.outputs.has_posts == 'true' | |
| uses: peaceiris/actions-hugo@v3 | |
| with: | |
| hugo-version: 'latest' | |
| extended: true | |
| - name: Build minified blog pages including upcoming posts | |
| if: steps.detect.outputs.has_posts == 'true' | |
| run: hugo --source docs/website --destination "${{ runner.temp }}/blog-mermaid-site" --buildFuture --minify | |
| - name: Validate blog Mermaid diagrams | |
| if: steps.detect.outputs.has_posts == 'true' | |
| run: | | |
| node scripts/website/validate_mermaid.mjs \ | |
| --site "${{ runner.temp }}/blog-mermaid-site" \ | |
| --browser chromium --browser firefox docs/website/content/blog | |
| - name: Upload net-new findings report | |
| if: ${{ !cancelled() && steps.detect.outputs.has_posts == 'true' }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: blog-prose-net-new | |
| path: build/blog-prose/net-new.json | |
| if-no-files-found: warn | |
| - name: Comment net-new findings on PR | |
| if: ${{ !cancelled() && steps.detect.outputs.has_posts == 'true' && !github.event.pull_request.head.repo.fork }} | |
| uses: actions/github-script@v9 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const fs = require('fs'); | |
| const marker = '<!-- blog-prose-gate -->'; | |
| let report = { total: 0, findings: [] }; | |
| try { report = JSON.parse(fs.readFileSync('build/blog-prose/net-new.json', 'utf8')); } catch (e) {} | |
| const lines = [marker, '## Blog prose gate', '']; | |
| if (report.total === 0) { | |
| lines.push('✅ No net-new prose findings introduced by this PR.'); | |
| } else { | |
| lines.push(`❌ ${report.total} net-new prose finding(s) introduced by this PR. Pre-existing issues in the same posts are ignored.`, ''); | |
| for (const f of report.findings.slice(0, 50)) { | |
| lines.push(`- \`${f.file}:${f.line}\` — ${f.message}`); | |
| } | |
| if (report.findings.length > 50) lines.push(`- …and ${report.findings.length - 50} more (see the blog-prose-net-new artifact).`); | |
| lines.push('', 'Fix the prose, add a term to `scripts/website/languagetool-accept-blog.txt` or the CodenameOne Vale vocab, or add a `<!-- vale-skip: rule: reason -->` comment.'); | |
| } | |
| const body = lines.join('\n'); | |
| const { owner, repo } = context.repo; | |
| const issue_number = context.issue.number; | |
| const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number, per_page: 100 }); | |
| const existing = comments.find(c => c.body && c.body.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); | |
| } else { | |
| await github.rest.issues.createComment({ owner, repo, issue_number, body }); | |
| } | |
| - name: Fail on net-new findings | |
| if: ${{ !cancelled() }} | |
| run: | | |
| status="${{ steps.gate.outputs.status }}" | |
| if [ "${{ steps.detect.outputs.has_posts }}" != "true" ]; then | |
| echo "No blog content changed — prose gate passes." | |
| exit 0 | |
| fi | |
| if [ -n "$status" ] && [ "$status" != "0" ]; then | |
| echo "Blog prose gate found net-new findings. See the log and the blog-prose-net-new artifact." >&2 | |
| exit 1 | |
| fi | |
| echo "Blog prose gate passed." | |
| # --------------------------------------------------------------------------- # | |
| # Weekly / on-demand whole-corpus backlog report. NEVER fails the build. | |
| # --------------------------------------------------------------------------- # | |
| report: | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v6 | |
| - name: Set up Python 3 | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.12' | |
| - name: Install Vale | |
| run: | | |
| set -euo pipefail | |
| ARCHIVE="vale_${VALE_VERSION}_Linux_64-bit.tar.gz" | |
| curl -fsSL -o "$ARCHIVE" "https://github.com/errata-ai/vale/releases/download/v${VALE_VERSION}/${ARCHIVE}" | |
| tar -xzf "$ARCHIVE" | |
| sudo mv vale /usr/local/bin/vale | |
| rm -f "$ARCHIVE" | |
| - name: Sync Vale styles | |
| run: vale sync --config docs/website/.vale.ini | |
| - name: Whole-corpus Vale + capitalization report | |
| run: | | |
| set -euo pipefail | |
| mkdir -p build/blog-prose | |
| vale --config docs/website/.vale.ini --minAlertLevel=suggestion --output=JSON \ | |
| docs/website/content/blog > build/blog-prose/vale-corpus.json 2>/dev/null || true | |
| python3 scripts/website/check_paragraph_capitalization.py \ | |
| --output build/blog-prose/capitalization-corpus.json \ | |
| docs/website/content/blog || true | |
| python3 - <<'PY' >> "$GITHUB_STEP_SUMMARY" | |
| import json, collections | |
| d = json.load(open('build/blog-prose/vale-corpus.json')) | |
| by_rule = collections.Counter() | |
| files = set() | |
| for f, items in d.items(): | |
| for it in items: | |
| by_rule[it['Check']] += 1; files.add(f) | |
| cap = json.load(open('build/blog-prose/capitalization-corpus.json')) | |
| print('## Blog prose backlog report\n') | |
| print(f'Vale: **{sum(by_rule.values())}** findings across **{len(files)}** posts.\n') | |
| print('| Rule | Count |\n|---|---|') | |
| for r, n in by_rule.most_common(25): | |
| print(f'| {r} | {n} |') | |
| print(f'\nParagraph capitalization: **{cap["total"]}** findings.') | |
| PY | |
| - name: Upload backlog report | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: blog-prose-backlog-report | |
| path: build/blog-prose/ | |
| if-no-files-found: warn |