Skip to content
Open
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
60 changes: 60 additions & 0 deletions .githooks/install-hooks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# install-hooks.sh – Symlink the project's Git hooks into .git/hooks/.
#
# Run this script once after cloning the repository to activate the
# pre-commit accessibility checks provided by A11yAgent.
#
# Usage:
# bash .githooks/install-hooks.sh
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
GIT_HOOKS_DIR="$REPO_ROOT/.git/hooks"

if [[ ! -d "$GIT_HOOKS_DIR" ]]; then
echo "install-hooks: .git/hooks directory not found at $GIT_HOOKS_DIR"
echo " Make sure you are running this script from within the repository."
exit 1
fi

install_hook() {
local name="$1"
local source="$SCRIPT_DIR/$name"
local target="$GIT_HOOKS_DIR/$name"

if [[ ! -f "$source" ]]; then
echo "install-hooks: source hook not found: $source"
return 1
fi

# Make the source hook executable.
chmod +x "$source"

# Back up any existing hook that is not already our symlink.
if [[ -e "$target" && ! -L "$target" ]]; then
local backup="$target.bak"
echo "install-hooks: backing up existing $name hook to $target.bak"
mv "$target" "$backup"
elif [[ -L "$target" ]]; then
# Remove a stale symlink so we can recreate it cleanly.
rm "$target"
fi

ln -s "$source" "$target"
echo "install-hooks: installed $name → $target"
}

install_hook "pre-commit"

echo ""
echo "Git hooks installed successfully."
echo ""
echo "The pre-commit hook will run A11yAgent accessibility checks on staged"
echo "Kotlin files before each commit."
echo ""
echo "To build the A11yAgent JAR (required by the hook):"
echo " ./gradlew :A11yAgent:shadowJar"
echo ""
echo "To skip the hook for a single commit:"
echo " git commit --no-verify"
89 changes: 89 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# pre-commit – Run A11yAgent accessibility checks on staged Kotlin files.
#
# Copies the staged (index) version of each modified/added .kt file into a
# temporary directory and runs the A11yAgent shadow JAR against that snapshot.
# Warnings are shown but do not block the commit; errors cause an exit 1.
#
# To skip this hook for a single commit:
# git commit --no-verify
#
# To build the JAR if it is missing:
# ./gradlew :A11yAgent:shadowJar
set -euo pipefail

JAR_PATH="A11yAgent/build/libs/a11y-check-android-0.1.0.jar"

# ---------------------------------------------------------------------------
# 1. Collect staged .kt files (added, copied, or modified – not deleted).
# ---------------------------------------------------------------------------
KT_FILES=()
while IFS= read -r line; do
KT_FILES+=("$line")
done < <(git diff --cached --name-only --diff-filter=ACM | grep '\.kt$' || true)

if [[ ${#KT_FILES[@]} -eq 0 ]]; then
exit 0
fi

# ---------------------------------------------------------------------------
# 2. Verify the shadow JAR is available.
# ---------------------------------------------------------------------------
if [[ ! -f "$JAR_PATH" ]]; then
echo ""
echo "pre-commit: A11yAgent JAR not found at $JAR_PATH"
echo " Build it first with: ./gradlew :A11yAgent:shadowJar"
echo " To skip this check: git commit --no-verify"
echo ""
exit 1
fi

# ---------------------------------------------------------------------------
# 3. Mirror staged file content into a temp directory.
# ---------------------------------------------------------------------------
TMPDIR_HOOK="$(mktemp -d)"
trap 'rm -rf "$TMPDIR_HOOK"' EXIT

for f in "${KT_FILES[@]}"; do
dest="$TMPDIR_HOOK/$f"
mkdir -p "$(dirname "$dest")"
# Read the staged (index) version, not the working-tree version.
git show ":$f" > "$dest"
done

# ---------------------------------------------------------------------------
# 4. Run the accessibility checker.
# ---------------------------------------------------------------------------
echo ""
echo "pre-commit: Running A11yAgent accessibility checks on ${#KT_FILES[@]} staged Kotlin file(s)…"
echo ""

# Capture output; strip the temp-dir prefix so paths look project-relative.
RAW_OUTPUT="$(java -jar "$JAR_PATH" "$TMPDIR_HOOK" --format gradle 2>&1)" || true
OUTPUT="$(printf '%s\n' "$RAW_OUTPUT" | sed "s|$TMPDIR_HOOK/||g")"

# ---------------------------------------------------------------------------
# 5. Evaluate results.
# ---------------------------------------------------------------------------
HAS_ERRORS=0

if printf '%s\n' "$OUTPUT" | grep -q '^.*: error:'; then
HAS_ERRORS=1
fi

if [[ -n "$OUTPUT" ]]; then
printf '%s\n' "$OUTPUT"
echo ""
fi

if [[ $HAS_ERRORS -eq 1 ]]; then
echo "pre-commit: Accessibility errors found – commit blocked."
echo " Fix the errors above, then re-stage your files and commit."
echo " To skip this check: git commit --no-verify"
echo ""
exit 1
fi

echo "pre-commit: Accessibility checks passed."
echo ""
exit 0
110 changes: 110 additions & 0 deletions .github/workflows/a11y-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
name: A11y Check

on:
pull_request:
paths:
- '**/*.kt'
push:
branches: [main]
paths:
- '**/*.kt'

env:
MIN_SCORE: 70

jobs:
a11y-check:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
security-events: write

steps:
- uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Build a11y-check-android
run: ./gradlew :A11yAgent:shadowJar

- name: Run a11y-check (JSON)
id: a11y_json
run: |
java -jar A11yAgent/build/libs/a11y-check-android-*.jar \
app/src/main/java \
--format json > a11y-results.json || true
echo "score=$(python3 -c 'import json,sys; d=json.load(open("a11y-results.json")); print(d.get("score",{}).get("score",0))')" >> "$GITHUB_OUTPUT"
echo "grade=$(python3 -c 'import json,sys; d=json.load(open("a11y-results.json")); print(d.get("score",{}).get("grade","?"))')" >> "$GITHUB_OUTPUT"
echo "errors=$(python3 -c 'import json,sys; d=json.load(open("a11y-results.json")); print(d.get("score",{}).get("totalErrors",0))')" >> "$GITHUB_OUTPUT"
echo "warnings=$(python3 -c 'import json,sys; d=json.load(open("a11y-results.json")); print(d.get("score",{}).get("totalWarnings",0))')" >> "$GITHUB_OUTPUT"
echo "failed_criteria=$(python3 -c '
import json
d=json.load(open("a11y-results.json"))
fc=d.get("score",{}).get("failedCriteria",[])
if fc:
print("\\n".join(["- " + c for c in fc]))
else:
print("None")
')" >> "$GITHUB_OUTPUT"

- name: Run a11y-check (SARIF)
run: |
java -jar A11yAgent/build/libs/a11y-check-android-*.jar \
app/src/main/java \
--format sarif > a11y-results.sarif || true

- name: Upload SARIF to GitHub Code Scanning
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: a11y-results.sarif
category: a11y-check

- name: Upload results artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: a11y-results
path: |
a11y-results.json
a11y-results.sarif

- name: Post PR Comment
if: github.event_name == 'pull_request'
uses: marocchino/sticky-pull-request-comment@v2
with:
header: a11y-check
message: |
## Accessibility Score: ${{ steps.a11y_json.outputs.score }}/100 (${{ steps.a11y_json.outputs.grade }})

| Metric | Count |
|--------|-------|
| Errors | ${{ steps.a11y_json.outputs.errors }} |
| Warnings | ${{ steps.a11y_json.outputs.warnings }} |

**Failed WCAG Criteria:**
${{ steps.a11y_json.outputs.failed_criteria }}

<details>
<summary>Run details</summary>

Generated by `a11y-check-android` — WCAG 2.2 static analysis for Jetpack Compose.
Minimum score threshold: ${{ env.MIN_SCORE }}
</details>

- name: Check minimum score
run: |
score="${{ steps.a11y_json.outputs.score }}"
if [ "$(echo "$score < $MIN_SCORE" | bc -l)" -eq 1 ]; then
echo "::error::Accessibility score $score is below minimum threshold $MIN_SCORE"
exit 1
fi
echo "Accessibility score $score meets minimum threshold $MIN_SCORE"
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,10 @@
.externalNativeBuild
.cxx
local.properties

# a11y-check score tracking and baseline files
.a11y-scores.json
.a11y-baseline.json

# Gradle daemon JVM properties (auto-generated)
gradle/gradle-daemon-jvm.properties
Loading