Skip to content

draft(a11y): evaluate hover tooltip as supplemental new-window warning #9

draft(a11y): evaluate hover tooltip as supplemental new-window warning

draft(a11y): evaluate hover tooltip as supplemental new-window warning #9

# github/codeql-action cannot run inside a required workflow -- GitHub
# refuses to admit it, 0/43+ across every sampled repository
# (docs/doctoring/codeql-pr-required-workflow-always-fails.md). This file
# stays required-workflow-safe by never calling codeql-action itself: it
# detects languages, dispatches the actual scan via repository_dispatch to
# codeql-scan-dispatch.yml (which runs natively, unrestricted, in
# ContextualWisdomLab/.github), and polls for a codeql-dispatch/<language>
# commit status that handler publishes back onto this PR's head. Design:
# docs/adr/0025-codeql-required-workflow-dispatch-architecture.md. The
# merge-preview scan (analyze-merge) is required nowhere (PR #1766) and was
# dropped, not migrated.
name: CodeQL PR
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, closed]
# Do not restrict the base ref: the org required-workflow ruleset already
# scopes this to each repository's actual default branch via
# ref_name: ["~DEFAULT_BRANCH"], whatever it is named. A hardcoded
# [main, master, develop] list silently produced zero CodeQL checks for
# any repository with a different default branch name (confirmed live:
# a repository defaulting to gh-pages received every other required
# check but no CodeQL check at all) and would also block coverage for
# stacked PRs targeting a non-default feature branch, matching
# security-scan.yml's own "do not restrict the base ref" precedent.
concurrency:
# NOT scoped by head SHA, unlike opencode-review.yml's group -- and that is
# a deliberate, tested difference, not an oversight. This file has no
# dedicated cancel-on-close cleanup job (see
# tests/test_required_workflow_queue_contract.py::test_pull_request_close_events_cancel_superseded_runs_without_heavy_jobs),
# so this group's own `cancel-in-progress: true` is the ONLY mechanism that
# cancels a stale in-flight run when the PR closes. opencode-review.yml can
# safely add head SHA to its group because it ALSO runs a separate
# cancel-superseded-opencode-review-runs job that sweeps stale runs via
# direct API calls regardless of head SHA; adding head SHA here without an
# equivalent job would let an older, still-in-flight run for a since-
# superseded head survive a close event indefinitely (it and the closing
# run would land in different groups and never cancel each other). A
# narrower risk remains -- a delayed dispatch for an older head could still
# transiently evict a newer head's in-flight poll before that older run's
# own live-head recheck self-aborts -- tracked as a follow-up requiring a
# dedicated cleanup job, not a one-line group change.
group: >-
codeql-pr-${{
github.event_name == 'pull_request' && github.event.pull_request.base.repo.full_name || github.repository }}-${{
github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
detect-languages:
name: Detect CodeQL languages
if: github.event.action != 'closed'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
matrix: ${{ steps.detect.outputs.matrix }}
code: ${{ steps.scope.outputs.code }}
steps:
- name: Checkout PR head
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
ref: ${{ github.event.pull_request.head.sha }}
- name: Build language matrix
id: detect
run: |
matrix='[]'
if [ -d .github/workflows ]; then
matrix=$(echo "$matrix" | jq -c '. + [{"language":"actions","build-mode":"none"}]')
fi
if find . -type f \( -name '*.js' -o -name '*.jsx' -o -name '*.ts' -o -name '*.tsx' \) \
-not -path './.git/*' -print -quit | grep -q .; then
matrix=$(echo "$matrix" | jq -c '. + [{"language":"javascript-typescript","build-mode":"none"}]')
fi
if find . -type f -name '*.py' -not -path './.git/*' -print -quit | grep -q .; then
matrix=$(echo "$matrix" | jq -c '. + [{"language":"python","build-mode":"none"}]')
fi
if find . -type f \( -name '*.java' -o -name '*.kt' -o -name '*.kts' \) \
-not -path './.git/*' -print -quit | grep -q .; then
matrix=$(echo "$matrix" | jq -c '. + [{"language":"java-kotlin","build-mode":"none"}]')
fi
if [ "$(echo "$matrix" | jq 'length')" -eq 0 ]; then
matrix='[{"language":"actions","build-mode":"none"}]'
fi
{
echo 'matrix<<EOF'
jq -nc --argjson include "$matrix" '{include: $include}'
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- name: Classify changed paths
id: scope
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.event.pull_request.base.repo.full_name || github.repository }}
PR: ${{ github.event.pull_request.number }}
EXPECTED_FILES: ${{ github.event.pull_request.changed_files }}
shell: bash
run: |
set -uo pipefail
code=true
if [ -n "${PR}" ] && [ -n "${EXPECTED_FILES}" ]; then
changed=""
for attempt in 1 2 3; do
if changed="$(gh api --paginate "repos/${REPO}/pulls/${PR}/files?per_page=100" --jq '.[].filename')" && [ -n "$changed" ]; then
break
fi
changed=""
sleep $((attempt * 3))
done
# GitHub caps /pulls/N/files at 3000 entries; a short list would hide
# source files behind a doc-only verdict, so require an exact count.
if [ -n "$changed" ] && [ "$(printf '%s\n' "$changed" | wc -l | tr -d ' ')" = "${EXPECTED_FILES}" ]; then
code=false
while IFS= read -r changed_path; do
case "$changed_path" in
*.md|*.markdown|*.rst|*.png|*.jpg|*.jpeg|*.gif|*.webp|*.bmp|*.ico|LICENSE|LICENSE.txt|COPYING|COPYING.txt|NOTICE|NOTICE.txt|.github/ISSUE_TEMPLATE/*) ;;
*) code=true ;;
esac
done <<<"$changed"
else
echo "::notice::changed-scope could not read a complete PR file list; scanning everything."
fi
fi
echo "code=${code}" >> "$GITHUB_OUTPUT"
echo "changed-scope code=${code}"
analyze-head:
name: CodeQL compatibility analysis (${{ matrix.language }})
needs: detect-languages
# No job-level `if:` on purpose: a job-level condition referencing
# needs.detect-languages.outputs.* skips this job before its
# matrix-derived name is expanded, publishing the literal
# `CodeQL compatibility analysis (${{ matrix.language }})` check-run name
# instead of one per real language -- decisive live evidence in run
# 33708209086, guarded by
# tests/test_docs_only_pr_runner_admission.py::test_codeql_pr_gates_analyze_head_at_step_level_not_job_level.
# `needs: detect-languages` (only) matches the original, proven-safe
# dependency exactly; the only case where it's genuinely skipped is a
# closed PR, where this job being implicitly skipped too is fine because
# closed PRs need no required check.
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.detect-languages.outputs.matrix) }}
steps:
- name: Request current-head CodeQL scan dispatch
# Dispatch+poll live as sequential steps of ONE job (mirroring
# opencode-review.yml's opencode-review-target job) specifically so a
# dispatch failure fails this job directly -- no needs-based skip to
# worry about, and (below) the poll step can read this step's own
# `outcome` within the same shard. Each shard dispatches only ITS OWN
# language (not the full matrix): dispatching the full matrix from a
# single shard would leave every OTHER shard blind to that one
# shard's dispatch failure, each polling the full 3-hour deadline
# before self-timing-out for a scan that was never actually
# requested. One dispatch per language costs the same total .github-side
# work as one dispatch carrying every language (N single-language
# scans either way) while letting every shard fail closed immediately
# on its own dispatch failure instead of only detecting it 3 hours
# later.
id: dispatch
if: needs.detect-languages.outputs.code == 'true'
env:
GH_TOKEN: ${{ github.token }}
OIDC_AUDIENCE: opencode-github-action
OPENCODE_API_BASE_URL: https://api.opencode.ai
TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
LANGUAGE: ${{ matrix.language }}
BUILD_MODE: ${{ matrix.build-mode }}
run: |
set -euo pipefail
live_pr="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")"
live_head="$(printf '%s' "$live_pr" | jq -r '.head.sha // empty')"
live_state="$(printf '%s' "$live_pr" | jq -r 'if (.state | type) == "string" then .state else empty end')"
if [ -z "$live_head" ] || [ -z "$live_state" ]; then
echo "::error::Could not validate live pull request state before CodeQL dispatch."
exit 1
fi
if [ "$live_state" = "closed" ]; then
echo "PR is closed on the live exact head; a current-head CodeQL scan is not requested."
exit 0
fi
if [ "${live_head,,}" != "${PR_HEAD_SHA,,}" ]; then
echo "Pull request head moved on the live open PR; a fresh dispatch will fire for the current head."
exit 0
fi
if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then
echo "::error::CodeQL scan dispatch requires GitHub OIDC."
exit 1
fi
separator='&'
[[ "$ACTIONS_ID_TOKEN_REQUEST_URL" == *\?* ]] || separator='?'
oidc_token="$(curl -fsS -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" "${ACTIONS_ID_TOKEN_REQUEST_URL}${separator}audience=${OIDC_AUDIENCE}" | jq -r '.value // empty')"
if [ -z "$oidc_token" ]; then
echo "::error::CodeQL scan dispatch could not obtain its OIDC token."
exit 1
fi
app_token="$(curl -fsS -X POST -H "Authorization: Bearer ${oidc_token}" "${OPENCODE_API_BASE_URL}/exchange_github_app_token" | jq -r '.token // empty')"
if [ -z "$app_token" ]; then
echo "::error::CodeQL scan dispatch could not obtain its repository-scoped app token."
exit 1
fi
echo "::add-mask::$app_token"
jq -cn \
--arg target_repository "$TARGET_REPOSITORY" \
--arg pr_number "$PR_NUMBER" \
--arg pr_base_ref "$PR_BASE_REF" \
--arg pr_base_sha "$PR_BASE_SHA" \
--arg pr_head_ref "$PR_HEAD_REF" \
--arg pr_head_sha "$PR_HEAD_SHA" \
--arg language "$LANGUAGE" \
--arg build_mode "$BUILD_MODE" \
'{event_type:"codeql-scan",client_payload:{target_repository:$target_repository,pr_number:$pr_number,pr_base_ref:$pr_base_ref,pr_base_sha:$pr_base_sha,pr_head_ref:$pr_head_ref,pr_head_sha:$pr_head_sha,matrix:[{language:$language,"build-mode":$build_mode}]}}' |
GH_TOKEN="$app_token" gh api -X POST repos/ContextualWisdomLab/.github/dispatches --input -
- name: Fail closed without a current-head CodeQL dispatch verdict
if: needs.detect-languages.outputs.code == 'true'
env:
GH_TOKEN: ${{ github.token }}
TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
LANGUAGE: ${{ matrix.language }}
DISPATCH_OUTCOME: ${{ steps.dispatch.outcome }}
run: |
set -euo pipefail
if [ "$DISPATCH_OUTCOME" != "success" ]; then
echo "::error::CodeQL scan dispatch did not succeed (outcome=${DISPATCH_OUTCOME}); failing closed without polling."
exit 1
fi
poll_interval_seconds=30
max_poll_transport_failures=3
poll_failures=0
# Wall-clock backstop distinct from max_poll_transport_failures:
# that counter only bounds *consecutive transport failures*, so a
# dispatched scan that never posts a status -- while every
# individual `gh api` call keeps succeeding -- would otherwise poll
# forever. Mirrors opencode-review.yml's identical 3-hour bound.
poll_deadline_epoch=$(( $(date -u +%s) + 10800 ))
while :; do
if [ "$(date -u +%s)" -ge "$poll_deadline_epoch" ]; then
echo "::error::No current-head CodeQL dispatch verdict after 180 minutes of polling; failing closed and releasing the runner."
exit 1
fi
if ! live_pr="$(timeout 30s gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")"; then
poll_failures=$((poll_failures + 1))
if [ "$poll_failures" -ge "$max_poll_transport_failures" ]; then
echo "::error::Live pull request read failed ${poll_failures} consecutive times while polling; failing closed and releasing the runner."
exit 1
fi
echo "::warning::Live pull request read failed while polling (${poll_failures}/${max_poll_transport_failures}); retrying after revalidation delay."
sleep "$poll_interval_seconds"
continue
fi
poll_failures=0
live_head="$(printf '%s' "$live_pr" | jq -r '.head.sha // empty')"
live_state="$(printf '%s' "$live_pr" | jq -r 'if (.state | type) == "string" then .state else empty end')"
if [ -z "$live_head" ] || [ -z "$live_state" ]; then
echo "::error::Could not validate live pull request state while polling for a current-head CodeQL verdict."
exit 1
fi
if [ "${live_head,,}" != "${HEAD_SHA,,}" ]; then
echo "::notice::Pull request head moved while waiting for a current-head CodeQL verdict; retiring superseded poll."
exit 0
fi
if [ "$live_state" = "closed" ]; then
echo "PR closed while waiting for the current-head CodeQL verdict; the poll is no longer required."
exit 0
fi
if ! statuses="$(timeout 30s gh api "repos/${TARGET_REPOSITORY}/commits/${HEAD_SHA}/statuses")"; then
poll_failures=$((poll_failures + 1))
if [ "$poll_failures" -ge "$max_poll_transport_failures" ]; then
echo "::error::Commit statuses read failed ${poll_failures} consecutive times while polling; failing closed and releasing the runner."
exit 1
fi
echo "::warning::Commit statuses read failed while polling (${poll_failures}/${max_poll_transport_failures}); revalidating live PR state before retry."
sleep "$poll_interval_seconds"
continue
fi
poll_failures=0
# A commit status is writable by anyone with statuses:write on
# this repository, so matching on .context alone would let a
# malicious PR forge its own passing "codeql-dispatch/<language>"
# status and skip being scanned (ADR 0025, "Poll target cannot be
# spoofed by the PR author"). codeql-scan-dispatch.yml mints its
# publishing token via the same OIDC audience
# (opencode-github-action) opencode-review-dispatch.yml uses, so
# the legitimate status always carries that app's bot identity --
# mirror opencode-review.yml's opencode-agent/opencode-agent[bot]
# creator check rather than trusting the context name alone.
verdict_state="$(printf '%s' "$statuses" | jq -r --arg ctx "codeql-dispatch/${LANGUAGE}" '
[
.[]
| select(.context == $ctx)
| select(
(.creator.login // "" | ascii_downcase) as $creator
| $creator == "opencode-agent" or $creator == "opencode-agent[bot]"
)
]
| first // {} | .state // empty
')"
if [ "$verdict_state" = "success" ] || [ "$verdict_state" = "failure" ] || [ "$verdict_state" = "error" ]; then
break
fi
sleep "$poll_interval_seconds"
done
if [ "$verdict_state" != "success" ]; then
echo "::error::CodeQL dispatch scan for ${LANGUAGE} did not pass (state=${verdict_state}). See the linked dispatch run (codeql-scan-dispatch.yml in ContextualWisdomLab/.github) for SARIF evidence."
exit 1
fi
echo "Current-head CodeQL dispatch verdict for ${LANGUAGE}: success."