Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a83d6d3
fix(security): allowlist https://api.github.com before urllib urlopen
seonghobae Sep 18, 2026
225260a
test(security): pin GitHub API redirect credential boundary
seonghobae Sep 18, 2026
0ae2204
fix(security): contain GitHub API redirects to admitted origin
seonghobae Sep 18, 2026
062663a
test(security): pin Strix redirect credential boundary
seonghobae Sep 18, 2026
2708a6b
fix(security): contain Strix GitHub API redirects
seonghobae Sep 18, 2026
3758b89
chore(deps): bump anyio from 4.14.0 to 4.14.2
dependabot[bot] Sep 18, 2026
4dcd25c
test(security): align Strix transport seam with dedicated opener
seonghobae Sep 19, 2026
834d285
test(security): bind authenticated openers at owned seams
seonghobae Sep 19, 2026
6d10002
merge(security): carry #2269 urllib GitHub API opener into #2278
seonghobae Sep 19, 2026
545648e
chore(deps): isolate AnyIO security owner delta
seonghobae Sep 19, 2026
c51c8d2
merge: carry AnyIO lock update onto protected owner
seonghobae Sep 19, 2026
e43a75b
fix(opencode): materialize every coverage lock input
seonghobae Sep 19, 2026
c4a73a1
docs(gap): bind coverage repair to canonical PR
seonghobae Sep 19, 2026
8a5251b
fix(deps): restore AnyIO owner isolation
seonghobae Sep 20, 2026
657d104
repair(codeql): restore exact endpoint-set assertion
seonghobae Sep 23, 2026
f1a8dc8
chore(foundation): converge dependency and CodeQL repairs
seonghobae Sep 23, 2026
42e4198
fix(opencode): adopt AnyIO security prerequisite
seonghobae Sep 24, 2026
38cfcae
fix(test-gate): restore full branch coverage
seonghobae Sep 24, 2026
21247ef
test(queue-health): scope permission assertion to workflow token
seonghobae Sep 24, 2026
57c168b
test(queue-health): isolate collector edge cases
seonghobae Sep 24, 2026
f229816
repair(foundation): converge CodeQL endpoint contract into coverage o…
seonghobae Sep 24, 2026
3b3ce16
fix(security): update AnyIO lock for audit gate
seonghobae Sep 25, 2026
3d03521
fix(dispatch): retire stale CodeQL/OpenCode dispatches as notices ins…
seonghobae Sep 25, 2026
14fe2b6
fix(codeql): unblock CodeQL scan dispatch for all .github PRs
seonghobae Sep 26, 2026
950ab88
repair(foundation): converge coverage, dependency, and CodeQL owners
seonghobae Sep 26, 2026
8e1aba9
fix(ci): bind AnyIO security pin to Strix input
seonghobae Sep 26, 2026
d9e3132
merge: stack stale-dispatch repair on foundation
seonghobae Sep 26, 2026
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
110 changes: 109 additions & 1 deletion .github/workflows/codeql-scan-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ jobs:
rerun_schema: ${{ steps.validate.outputs.rerun_schema }}
producer_source_sha: ${{ steps.validate.outputs.producer_source_sha }}
dispatch_protocol: ${{ steps.validate.outputs.dispatch_protocol }}
stale: ${{ steps.validate.outputs.stale }}
steps:
- name: Exchange OpenCode app token for target repository metadata reads
id: metadata_read_app_token
Expand Down Expand Up @@ -365,6 +366,64 @@ jobs:
live_merge_commit_sha="$(jq -r '.merge_commit_sha // empty' <<<"$pull_request_json")"
live_state="$(jq -r '.state // empty' <<<"$pull_request_json")"

# Stale-dispatch retirement. Under runner-queue saturation this run can
# start hours after the dispatched head was superseded or the pull
# request closed; the newer dispatch is itself still queued, so the
# workflow-level cancel-in-progress group cannot retire this run first.
# Only a well-formed live answer proving the dispatched head is no
# longer current ends the run as a notice. A failed `gh api` lookup has
# already exited non-zero above, and every other disagreement below
# (base, head ref, fork, malformed metadata) stays fail-closed.
# No EVENT_NAME guard is needed here, unlike opencode-review-dispatch.yml:
# this workflow is triggered only by repository_dispatch and the
# actor/sender authorization above is unconditional, so every run that
# reaches this point is an authorized dispatch. The OpenCode guard exists
# because its authorization block is itself nested under that event check.
stale_reason=""
if [ "$live_state" = "closed" ]; then
stale_reason="pull request is closed"
elif [ "$live_state" = "open" ] &&
[[ "$live_head_sha" =~ ^[0-9a-fA-F]{40}$ ]] &&
[[ "$SUPPLIED_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]] &&
[ "${SUPPLIED_HEAD_SHA,,}" != "${live_head_sha,,}" ]; then
# A mismatch alone is not proof: right after a push the API can
# briefly serve the previous head, which would retire a brand-new
# dispatch. Retire only when the compare API proves the dispatched
# head is a strict ancestor of the live head (status "ahead",
# behind_by 0), i.e. the live head is newer and gets its own
# dispatch. Behind, diverged (force-push), or a failed/unexpected
# compare falls through to the fail-closed head_sha mismatch below.
head_compare_status=""
if head_compare_json="$(gh api "repos/${TARGET_REPOSITORY}/compare/${SUPPLIED_HEAD_SHA}...${live_head_sha}" 2>/dev/null)"; then
head_compare_status="$(jq -r '
if .status == "ahead" and .behind_by == 0 and ((.ahead_by | type) == "number") and .ahead_by >= 1
then "proven-ancestor" else ((.status // "unknown") | tostring) end
' <<<"$head_compare_json" 2>/dev/null || true)"
fi
if [ "$head_compare_status" = "proven-ancestor" ]; then
stale_reason="pull request head moved ahead of the dispatched head"
else
printf 'Not retiring CodeQL dispatch for %s#%s as stale: dispatched head is not a proven ancestor of the live head (compare=%s).\n' "$TARGET_REPOSITORY" "$PR_NUMBER" "${head_compare_status:-unavailable}"
fi
fi
if [ -n "$stale_reason" ]; then
printf '::notice::Skipping stale CodeQL dispatch for %s#%s: %s (dispatched head=%s, live head=%s, state=%s).\n' "$TARGET_REPOSITORY" "$PR_NUMBER" "$stale_reason" "$SUPPLIED_HEAD_SHA" "${live_head_sha:-<missing>}" "$live_state"
# Backticks are literal Markdown code spans in the step summary.
# shellcheck disable=SC2016
printf -- '- Skipped stale CodeQL dispatch for %s#%s: %s (dispatched head `%s`, live head `%s`, state `%s`).\n' "$TARGET_REPOSITORY" "$PR_NUMBER" "$stale_reason" "$SUPPLIED_HEAD_SHA" "${live_head_sha:-<missing>}" "$live_state" >>"${GITHUB_STEP_SUMMARY:-/dev/null}"
{
printf 'stale=true\n'
printf 'target_repository=%s\n' "$TARGET_REPOSITORY"
printf 'pr_number=%s\n' "$PR_NUMBER"
# Keep the matrix well-formed so the skipped scan job's
# strategy never evaluates fromJSON on an empty string.
echo "matrix<<EOF"
printf '%s\n' "$matrix_json"
echo "EOF"
} >>"$GITHUB_OUTPUT"
exit 0
fi

if [ "$live_state" != "open" ] ||
[ "$live_base_repository" != "$TARGET_REPOSITORY" ] ||
[ "$live_head_repository" != "$TARGET_REPOSITORY" ] ||
Expand Down Expand Up @@ -407,6 +466,7 @@ jobs:
fi

{
printf 'stale=false\n'
printf 'target_repository=%s\n' "$TARGET_REPOSITORY"
printf 'pr_number=%s\n' "$PR_NUMBER"
printf 'base_ref=%s\n' "$live_base_ref"
Expand All @@ -430,6 +490,9 @@ jobs:
scan:
name: CodeQL dispatch scan (${{ matrix.language }})
needs: validate-dispatch
# A stale dispatch (closed PR or moved head) ends validate-dispatch with
# a notice; skipping here also skips settle-required-run via needs.
if: needs.validate-dispatch.outputs.stale != 'true'
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
Expand Down Expand Up @@ -587,11 +650,56 @@ jobs:
id: gate
run: python3 "$RUNNER_TEMP/codeql_sarif_gate.py" codeql-results-dispatch

- name: Select target CodeQL analysis-read credential
id: ghas_analysis_token
if: steps.gate.outcome == 'success'
env:
TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }}
TARGET_APP_TOKEN: ${{ steps.target_app_token.outputs.token || '' }}
PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '' }}
OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }}
WORKFLOW_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

probe_analysis_read() {
token_label="$1"
token="$2"
if [ -z "$token" ]; then
return 1
fi
if GH_TOKEN="$token" gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"repos/${TARGET_REPOSITORY}/code-scanning/analyses?per_page=1&tool_name=CodeQL" \
>/dev/null 2>&1; then
echo "::add-mask::$token"
{
printf 'token=%s\n' "$token"
printf 'source=%s\n' "$token_label"
} >>"$GITHUB_OUTPUT"
echo "Selected ${token_label} after proving target CodeQL analysis-read access."
return 0
fi
echo "::notice::${token_label} cannot read target CodeQL analyses; trying the next configured credential."
return 1
}

if probe_analysis_read "target-app-token" "$TARGET_APP_TOKEN" ||
probe_analysis_read "pr-review-merge-token" "$PR_REVIEW_MERGE_TOKEN" ||
probe_analysis_read "opencode-approve-token" "$OPENCODE_APPROVE_TOKEN" ||
probe_analysis_read "github-token" "$WORKFLOW_TOKEN"; then
exit 0
fi

echo "::error::no configured credential can read target CodeQL analyses; GHAS configuration identity cannot be proven."
exit 1

- name: Verify GHAS base/head CodeQL configuration identity
id: ghas_configuration_identity
if: steps.gate.outcome == 'success'
env:
GH_TOKEN: ${{ steps.target_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }}
GH_TOKEN: ${{ steps.ghas_analysis_token.outputs.token }}
TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }}
PR_NUMBER: ${{ needs.validate-dispatch.outputs.pr_number }}
BASE_REF: ${{ needs.validate-dispatch.outputs.base_ref }}
Expand Down
68 changes: 68 additions & 0 deletions .github/workflows/opencode-review-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ jobs:
head_ref: ${{ steps.validate.outputs.head_ref }}
head_sha: ${{ steps.validate.outputs.head_sha }}
is_private: ${{ steps.validate.outputs.is_private }}
stale: ${{ steps.validate.outputs.stale }}
steps:
- name: Exchange OpenCode app token for target repository metadata reads
id: metadata_read_app_token
Expand Down Expand Up @@ -221,6 +222,60 @@ jobs:
*) live_is_private="" ;;
esac

# Stale-dispatch retirement. Under runner-queue saturation this run can
# start hours after the dispatched head was superseded or the pull
# request closed; the newer dispatch is itself still queued, so the
# workflow-level cancel-in-progress group cannot retire this run first.
# Only a well-formed live answer proving the dispatched head is no
# longer current ends the run as a notice. A failed `gh api` lookup has
# already exited non-zero above, and every other disagreement below
# (base, head ref, fork, malformed metadata) stays fail-closed.
# The EVENT_NAME guard mirrors this script's shape: the actor/sender/
# target authorization above and the supplied-head binding below run
# only for repository_dispatch (other events bind to the live head), so
# retirement is reachable only on the authorized dispatch path.
stale_reason=""
if [ "$EVENT_NAME" != "repository_dispatch" ]; then
:
elif [ "$live_state" = "closed" ]; then
stale_reason="pull request is closed"
elif [ "$live_state" = "open" ] &&
[[ "$live_head_sha" =~ ^[0-9a-fA-F]{40}$ ]] &&
[[ "$SUPPLIED_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]] &&
[ "${SUPPLIED_HEAD_SHA,,}" != "${live_head_sha,,}" ]; then
# A mismatch alone is not proof: right after a push the API can
# briefly serve the previous head, which would retire a brand-new
# dispatch. Retire only when the compare API proves the dispatched
# head is a strict ancestor of the live head (status "ahead",
# behind_by 0), i.e. the live head is newer and gets its own
# dispatch. Behind, diverged (force-push), or a failed/unexpected
# compare falls through to the fail-closed head_sha mismatch below.
head_compare_status=""
if head_compare_json="$(gh api "repos/${TARGET_REPOSITORY}/compare/${SUPPLIED_HEAD_SHA}...${live_head_sha}" 2>/dev/null)"; then
head_compare_status="$(jq -r '
if .status == "ahead" and .behind_by == 0 and ((.ahead_by | type) == "number") and .ahead_by >= 1
then "proven-ancestor" else ((.status // "unknown") | tostring) end
' <<<"$head_compare_json" 2>/dev/null || true)"
fi
if [ "$head_compare_status" = "proven-ancestor" ]; then
stale_reason="pull request head moved ahead of the dispatched head"
else
printf 'Not retiring OpenCode review dispatch for %s#%s as stale: dispatched head is not a proven ancestor of the live head (compare=%s).\n' "$TARGET_REPOSITORY" "$PR_NUMBER" "${head_compare_status:-unavailable}"
fi
fi
if [ -n "$stale_reason" ]; then
printf '::notice::Skipping stale OpenCode review dispatch for %s#%s: %s (dispatched head=%s, live head=%s, state=%s).\n' "$TARGET_REPOSITORY" "$PR_NUMBER" "$stale_reason" "$SUPPLIED_HEAD_SHA" "${live_head_sha:-<missing>}" "$live_state"
# Backticks are literal Markdown code spans in the step summary.
# shellcheck disable=SC2016
printf -- '- Skipped stale OpenCode review dispatch for %s#%s: %s (dispatched head `%s`, live head `%s`, state `%s`).\n' "$TARGET_REPOSITORY" "$PR_NUMBER" "$stale_reason" "$SUPPLIED_HEAD_SHA" "${live_head_sha:-<missing>}" "$live_state" >>"${GITHUB_STEP_SUMMARY:-/dev/null}"
{
printf 'stale=true\n'
printf 'target_repository=%s\n' "$TARGET_REPOSITORY"
printf 'pr_number=%s\n' "$PR_NUMBER"
} >>"$GITHUB_OUTPUT"
exit 0
fi

if [ "$live_state" != "open" ] ||
[ "$live_base_repository" != "$TARGET_REPOSITORY" ] ||
! [[ "$live_head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] ||
Expand All @@ -246,6 +301,7 @@ jobs:
fi

{
printf 'stale=false\n'
printf 'target_repository=%s\n' "$TARGET_REPOSITORY"
printf 'pr_number=%s\n' "$PR_NUMBER"
printf 'base_ref=%s\n' "$live_base_ref"
Expand All @@ -260,6 +316,7 @@ jobs:
id: coverage_read_app_token
if: >-
github.event_name == 'repository_dispatch'
&& steps.validate.outputs.stale != 'true'
&& steps.validate.outputs.target_repository != ''
&& steps.validate.outputs.target_repository != github.repository
env:
Expand Down Expand Up @@ -327,6 +384,7 @@ jobs:
} >>"$GITHUB_OUTPUT"

- name: Materialize pull request merge tree for coverage measurement
if: steps.validate.outputs.stale != 'true'
env:
GH_TOKEN: ${{ steps.coverage_read_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }}
TARGET_REPOSITORY: ${{ steps.validate.outputs.target_repository }}
Expand Down Expand Up @@ -381,6 +439,7 @@ jobs:
tar -cf "$COVERAGE_SOURCE_ARCHIVE" -C "$COVERAGE_SOURCE_WORKDIR" .

- name: Upload materialized pull request merge tree
if: steps.validate.outputs.stale != 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: opencode-coverage-source
Expand All @@ -394,6 +453,7 @@ jobs:
if: >-
needs.validate-pr-metadata.result == 'success'
&& github.event_name == 'repository_dispatch'
&& needs.validate-pr-metadata.outputs.stale != 'true'
runs-on: ubuntu-24.04
timeout-minutes: 300
permissions:
Expand Down Expand Up @@ -632,12 +692,17 @@ jobs:
coverage_tool_image="opencode-coverage-tools:${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
coverage_build_dir="${RUNNER_TEMP}/opencode-coverage-tool-build"
trusted_ci_requirements="${GITHUB_WORKSPACE}/requirements-opencode-review-ci-hashes.txt"
trusted_noema_document_requirements="${GITHUB_WORKSPACE}/requirements-noema-document-ci-hashes.txt"
trusted_base_python_installer="${GITHUB_WORKSPACE}/scripts/ci/install_base_python_locks.py"
trusted_vcs_import_root_resolver="${GITHUB_WORKSPACE}/scripts/ci/resolve_opencode_base_vcs_import_root.sh"
if [ ! -f "$trusted_ci_requirements" ] || [ -L "$trusted_ci_requirements" ]; then
echo "::error::Trusted coverage requirements must be a regular non-symlink file."
exit 1
fi
if [ ! -f "$trusted_noema_document_requirements" ] || [ -L "$trusted_noema_document_requirements" ]; then
echo "::error::Trusted Noema document requirements must be a regular non-symlink file."
exit 1
fi
if [ ! -f "$trusted_base_python_installer" ] || [ -L "$trusted_base_python_installer" ]; then
echo "::error::Trusted base Python lock installer must be a regular non-symlink file."
exit 1
Expand All @@ -651,6 +716,8 @@ jobs:
chmod 0700 "$coverage_build_dir"
install -m 0644 "$trusted_ci_requirements" \
"$coverage_build_dir/requirements-opencode-review-ci-hashes.txt"
install -m 0644 "$trusted_noema_document_requirements" \
"$coverage_build_dir/requirements-noema-document-ci-hashes.txt"
install -m 0755 "$trusted_base_python_installer" \
"$coverage_build_dir/install-base-python-locks.py"
install -m 0755 "$trusted_vcs_import_root_resolver" \
Expand Down Expand Up @@ -2334,6 +2401,7 @@ jobs:
&& needs.validate-pr-metadata.result == 'success'
&& needs.coverage-evidence.result != 'cancelled'
&& github.event_name == 'repository_dispatch'
&& needs.validate-pr-metadata.outputs.stale != 'true'
concurrency:
group: >-
opencode-review-${{
Expand Down
23 changes: 23 additions & 0 deletions CHANGELOG.d/20260926-dispatch-stale-head-skip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
### Stale CodeQL/OpenCode dispatches end as a notice instead of failing

- `codeql-scan-dispatch.yml` (`validate-dispatch`) and `opencode-review-dispatch.yml`
(`validate-pr-metadata`) now retire a dispatch whose target pull request is closed, or whose
live head has moved strictly past the dispatched head, with a `::notice::`, a step-summary
line and a `stale=true` output. A head mismatch alone is not enough: the compare API
(`repos/{target}/compare/{dispatched}...{live}`) must answer `status: ahead` with
`behind_by: 0`, proving the live head descends from the dispatched one and therefore gets
its own dispatch. A lagging API that still serves the previous head right after a push
(`behind`), a force-push (`diverged`), or a failed or malformed compare keeps the original
fail-closed `head_sha` mismatch, so a fresh head is never skipped. The CodeQL `scan` matrix (and therefore `settle-required-run`)
and the OpenCode coverage-materialization steps, `coverage-evidence` and `opencode-review`
skip on that output, so the run concludes success instead of failure. Under runner-queue
saturation the newer dispatch for the same pull request is itself queued, so workflow-level
`cancel-in-progress` could not retire the stale run before it started.
- Still fail-closed: dispatch authorization and payload validation (checked first), a failed or
malformed live pull-request lookup, base ref/SHA or head ref disagreement at the same head,
cross-fork metadata, and the later privileged re-validation steps. Draft state is not treated
as stale because ruleset-launched required workflows do not re-dispatch on `ready_for_review`.
- The OpenCode side keeps retirement behind `EVENT_NAME == repository_dispatch` because its
dispatch authorization and supplied-head binding are nested under that check; the CodeQL
side needs no such guard because it triggers only on `repository_dispatch` and authorizes
unconditionally before the live lookup.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### OpenCode coverage image materializes every Dockerfile lock input

- Required OpenCode run `35370902053` for `.github#2266@12621f75e` failed before executing PR code because its trusted Dockerfile copied `requirements-noema-document-ci-hashes.txt` while the isolated build context contained only the OpenCode lockfile. The coverage owner now validates both lockfiles as regular non-symlink files and copies both into the trusted build context before the networked image build. `tests/test_opencode_agent_contract.py` pins the complete input boundary. Hosted exact-head acceptance remains Proposed until the new run reaches the image-build and coverage steps.

### Noema transport capacity schedules a bounded continuation re-dispatch

- After gateway failover, HTTP 429/5xx no longer end only as a permanent required-check failure with `caller attempts=1`. ADR-0031 classifies that class as `provider_capacity_unavailable`, keeps the single gateway request per job, surfaces `provider_attempt_count` from the orchestrator error envelope, and authorizes at most two same-head `repository_dispatch` retries after a capped `Retry-After` or deterministic 60–180 s jitter. Review is never skipped. Refs #2165.
Expand Down
Loading
Loading