Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/PULL_REQUEST_TEMPLATE/default.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Closes #
- [ ] Changes have been tested locally
- [ ] This PR is single-purpose and does not mix dependency/security updates with unrelated formatting or content cleanups
- [ ] All review threads are resolved or have an explicit won't-fix / follow-up disposition before merge
- [ ] Every accepted review finding that identifies a reproducible bug or edge case either lands a regression test in this PR **or** has a linked follow-up issue referenced in the thread resolution

## Additional Notes
<!-- Add any additional context or notes for reviewers -->
90 changes: 90 additions & 0 deletions .github/workflows/pr-hygiene-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: PR Hygiene Check

# Flags PRs that mix dependency/security changes with unrelated app or content
# changes — which the project checklist explicitly discourages.
#
# Runs in WARNING mode: the job always exits 0 so it never blocks merge.
# Switch `ENFORCE=true` to make it a hard failure once the pattern is stable.

on:
pull_request:
branches: ["main"]
types: [opened, synchronize, reopened, labeled, unlabeled]

env:
# Set to true to make the check block merges instead of warn
ENFORCE: "false"

jobs:
hygiene:
name: PR hygiene — dependency/security separation
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Check for mixed dependency/security + unrelated changes
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
PR_LABELS: ${{ toJson(github.event.pull_request.labels.*.name) }}
run: |
set -euo pipefail

# Skip if the override label is present
if echo "$PR_LABELS" | grep -qi "mixed-changes-ok"; then
echo "ℹ️ Label 'mixed-changes-ok' present — skipping hygiene check."
exit 0
fi

BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"

CHANGED=$(git diff --name-only "$BASE"..."$HEAD" 2>/dev/null || \
git diff --name-only "$BASE" "$HEAD")

# Count package files — use grep with || true so pipefail does not
# exit when grep finds zero matches (exit 1 = no match, not an error
# for our purposes).
DEP_FILES=$(echo "$CHANGED" | { grep -cE '^package(-lock)?\.json$' || true; })

# Detect security/dependency signals in title only (not body) to
# avoid false-positives from maintenance PRs that mention CVEs in
# context rather than as the fix target.
SEC_SIGNAL=0
if echo "$PR_TITLE" | grep -qiE 'cve-[0-9]|dependabot|security fix|bump .+ to [0-9]'; then
SEC_SIGNAL=1
fi

IS_DEP_OR_SEC=$(( DEP_FILES > 0 || SEC_SIGNAL > 0 ))

if [ "$IS_DEP_OR_SEC" -eq 0 ]; then
echo "✅ Not a dependency/security PR — no separation check needed."
exit 0
fi

# Count non-package files (app/content/docs/scripts)
UNRELATED=$(echo "$CHANGED" | { grep -cvE '^package(-lock)?\.json$' || true; })

if [ "$UNRELATED" -gt 0 ]; then
echo "⚠️ Mixed changes detected in a dependency/security PR:"
echo " Dependency/security files: package*.json or CVE/Dependabot signal in title"
echo " Other files changed ($UNRELATED):"
echo "$CHANGED" | grep -vE '^package(-lock)?\.json$' | sed 's/^/ /' || true
echo ""
echo " Project guidelines ask for single-purpose dependency/security PRs."
echo " To suppress this warning, add the 'mixed-changes-ok' label."
echo ""

if [ "$ENFORCE" = "true" ]; then
exit 1
else
echo " (Running in warning mode — this check does not block merge.)"
exit 0
fi
else
echo "✅ Dependency/security PR contains only package files — looks clean."
fi
56 changes: 52 additions & 4 deletions .github/workflows/weekly-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
node-version: "20"
cache: "npm"

- name: Install dependencies
- name: Install Node dependencies
run: npm ci

- name: Run tests
Expand All @@ -29,15 +29,63 @@ jobs:
- name: Run linting
run: npm run lint

# -----------------------------------------------------------------------
# Maintenance guardrails added in PRs #73/#76 — exercised weekly so
# toolchain drift and content-rules parity issues surface on a schedule
# rather than only when the relevant paths change.
# -----------------------------------------------------------------------

- name: Check content-rules trigger parity
run: python3 scripts/check-content-rules-trigger-parity.py

- name: Validate content rules
run: ./scripts/validate-content-rules.sh

- name: Run deploy-script regression tests
run: bash tests/deploy-script-regression.sh

- name: Run news-bullet and stale-search regression tests
run: npm test -- --runInBand tests/news-bullet-regression.test.js tests/command-palette-stale-search.test.js

- name: Check markdown quality for teaching content
run: |
# Only run if teaching content files exist; markdownlint errors are
# real failures — do not swallow them with || true.
if compgen -G "_teaching/**/*.md" > /dev/null 2>&1 || \
[ -f "assets/images/teaching/README.md" ]; then
npx markdownlint-cli2 --config .markdownlint-cli2.jsonc \
"_teaching/**/*.md" \
"assets/images/teaching/README.md"
else
echo "ℹ️ No teaching content files found — skipping markdownlint."
fi

# -----------------------------------------------------------------------
# Ruby / Jekyll toolchain check
# (install rbenv + pinned Ruby so check-ruby-toolchain.sh can verify)
# -----------------------------------------------------------------------

- name: Set up Ruby (pinned version)
uses: ruby/setup-ruby@v1
with:
ruby-version-file: ".ruby-version"
bundler-cache: true

- name: Check Ruby toolchain versions
run: bash scripts/check-ruby-toolchain.sh

- name: Jekyll build (Ruby + Node combined)
run: bundle exec jekyll build --destination _site_weekly_check

- name: Create test summary
if: always()
run: |
echo "## Weekly Test Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ ${{ job.status }} == 'success' ]; then
echo "✅ All tests passed successfully!" >> $GITHUB_STEP_SUMMARY
if [ "${{ job.status }}" = "success" ]; then
echo "✅ All checks passed successfully!" >> $GITHUB_STEP_SUMMARY
else
echo "❌ Some tests failed. Please check the logs above." >> $GITHUB_STEP_SUMMARY
echo "❌ Some checks failed. Please review the logs above." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "Test run completed on $(date)" >> $GITHUB_STEP_SUMMARY
Loading
Loading