draft(a11y): evaluate hover tooltip as supplemental new-window warning #9
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
| # 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." |