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
239 changes: 239 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
name: CI

on:
pull_request:
branches:
- main

permissions:
contents: read

jobs:
build:
name: Build Jekyll site
runs-on: ubuntu-latest
steps:
- name: Checkout repository (with submodules)
uses: actions/checkout@v4
with:
submodules: recursive

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
cache-version: 0

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libvips libvips-dev imagemagick

- name: Generate tag and category data
run: make generate

- name: Build Jekyll site
run: bundle exec jekyll build

- name: Upload built site artifact
uses: actions/upload-artifact@v4
with:
name: site
path: _site/
retention-days: 1
if-no-files-found: error

accessibility:
name: Accessibility (Pa11y)
runs-on: ubuntu-latest
needs: build
permissions:
contents: read
pull-requests: write
steps:
- name: Download built site artifact
uses: actions/download-artifact@v4
with:
name: site
path: _site/

- name: Install Pa11y
run: npm install -g pa11y@9

- name: Allow unprivileged user namespaces (Ubuntu 24.04 AppArmor workaround)
run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns

- name: Run Pa11y on representative pages
id: pa11y
run: |
FAILED=0

run_pa11y() {
local label="$1"
local subdir="$2"
local file
if [ -z "$subdir" ]; then
file=$(find _site/ -maxdepth 1 -name "index.html" 2>/dev/null | head -1)
else
file=$(find "_site/$subdir" -name "index.html" 2>/dev/null | sort 2>/dev/null | head -1)
fi
if [ -z "$file" ]; then
echo "SKIP [$label]: no file found at _site/$subdir"
echo "{\"pageUrl\":\"$file\",\"documentTitle\":\"$label\",\"issues\":[]}" > "/tmp/pa11y_${label}.json"
return
fi
echo ""
echo "=== $label: $file ==="
pa11y --reporter json --standard WCAG2AA "$file" > "/tmp/pa11y_${label}.json" 2>/dev/null || FAILED=1
}

run_pa11y "Homepage" ""
run_pa11y "Event" "events"
run_pa11y "Project" "projects"
run_pa11y "Person" "people"
run_pa11y "Organization" "organizations"
run_pa11y "Venue" "venues"
run_pa11y "Resource" "resources"
run_pa11y "Tag" "tags"
run_pa11y "Category" "categories"

echo "failed=$FAILED" >> "$GITHUB_OUTPUT"

- name: Build accessibility report
id: report
run: |
node - <<'EOF'
const fs = require('fs');
const path = require('path');

const labels = [
'Homepage', 'Event', 'Project', 'Person', 'Organization',
'Venue', 'Resource', 'Tag', 'Category'
];

let totalErrors = 0;
let totalWarnings = 0;
let sections = [];

for (const label of labels) {
const jsonPath = `/tmp/pa11y_${label}.json`;
if (!fs.existsSync(jsonPath)) continue;

let data;
try {
data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
} catch (e) {
sections.push(`### ${label}\n\n> Could not parse Pa11y output.\n`);
continue;
}

const issues = Array.isArray(data) ? data : (data.issues || []);
const errors = issues.filter(i => i.type === 'error');
const warnings = issues.filter(i => i.type === 'warning');
const notices = issues.filter(i => i.type === 'notice');
totalErrors += errors.length;
totalWarnings += warnings.length;

const pageUrl = (!Array.isArray(data) && data.pageUrl) ? data.pageUrl : label;
const title = (!Array.isArray(data) && data.documentTitle) ? ` — ${data.documentTitle}` : '';

if (issues.length === 0) {
sections.push(`### ${label}\n\n✅ No issues found.\n`);
} else {
let rows = [`### ${label}${title}`, '', `\`${pageUrl}\``, ''];
if (errors.length > 0) {
rows.push(`**${errors.length} error(s)**`, '');
for (const issue of errors) {
rows.push(`- **[${issue.code}]** ${issue.message}`);
rows.push(` - Context: \`${issue.context.replace(/`/g, "'").substring(0, 200)}\``);
rows.push(` - Selector: \`${issue.selector}\``);
rows.push('');
}
}
if (warnings.length > 0) {
rows.push(`<details><summary><strong>${warnings.length} warning(s)</strong></summary>`, '');
for (const issue of warnings) {
rows.push(`- **[${issue.code}]** ${issue.message}`);
rows.push(` - Context: \`${issue.context.replace(/`/g, "'").substring(0, 200)}\``);
rows.push(` - Selector: \`${issue.selector}\``);
rows.push('');
}
rows.push('</details>', '');
}
if (notices.length > 0) {
rows.push(`<details><summary>${notices.length} notice(s)</summary>`, '');
for (const issue of notices) {
rows.push(`- **[${issue.code}]** ${issue.message}`);
rows.push('');
}
rows.push('</details>', '');
}
sections.push(rows.join('\n'));
}
}

const statusEmoji = totalErrors > 0 ? '❌' : '✅';
const statusLine = totalErrors > 0
? `${totalErrors} error(s), ${totalWarnings} warning(s) found`
: `All pages passed — ${totalWarnings} warning(s)`;

const header = [
'<!-- pa11y-report -->',
'## Pa11y Accessibility Report',
'',
`${statusEmoji} **${statusLine}** (WCAG2AA)`,
'',
'---',
'',
].join('\n');

const report = header + sections.join('\n---\n\n');

fs.writeFileSync('/tmp/pa11y_report.md', report);
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, report);
console.log('Report written.');
EOF

- name: Post or update PR comment
if: github.event_name == 'pull_request'
uses: actions/github-script@v8
env:
REPORT_BODY_FILE: /tmp/pa11y_report.md
with:
script: |
const fs = require('fs');
const body = fs.readFileSync(process.env.REPORT_BODY_FILE, 'utf8');
const marker = '<!-- pa11y-report -->';

const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});

const existing = comments.data.find(c => c.body && c.body.includes(marker));

if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
console.log(`Updated existing comment ${existing.id}`);
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
console.log('Created new comment');
}

- name: Fail if accessibility errors were found
if: steps.pa11y.outputs.failed == '1'
run: |
echo "Pa11y found accessibility errors. See report above."
exit 1
Loading
Loading