Skip to content

Commit 3e09f1f

Browse files
authored
Merge pull request #487 from Lexus2016/sync/upstream-2026-06-23
sync: merge upstream/main (286 commits) — 2026-06-23
2 parents 7cafa44 + d8aaab8 commit 3e09f1f

492 files changed

Lines changed: 38575 additions & 10468 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Detect affected areas
2+
description: >-
3+
Classify a PR's changed files into CI work lanes (python, frontend, site,
4+
scan, deps, mcp_catalog) so the orchestrator can conditionally call only
5+
the sub-workflows a PR can affect. Outputs are always "true" on push/dispatch
6+
events and fail open (everything "true") when the diff cannot be computed.
7+
8+
outputs:
9+
python:
10+
description: Run Python tests / ruff / ty / windows-footguns.
11+
value: ${{ steps.classify.outputs.python }}
12+
frontend:
13+
description: Run the TypeScript typecheck matrix + desktop build.
14+
value: ${{ steps.classify.outputs.frontend }}
15+
docker_meta:
16+
description: Docker setup and meta files have changed.
17+
value: ${{ steps.classify.outputs.docker_meta }}
18+
site:
19+
description: Build the Docusaurus docs site.
20+
value: ${{ steps.classify.outputs.site }}
21+
scan:
22+
description: Run the supply-chain critical-pattern scanner.
23+
value: ${{ steps.classify.outputs.scan }}
24+
deps:
25+
description: Check pyproject.toml dependency upper bounds.
26+
value: ${{ steps.classify.outputs.deps }}
27+
mcp_catalog:
28+
description: Require MCP catalog security review label.
29+
value: ${{ steps.classify.outputs.mcp_catalog }}
30+
31+
runs:
32+
using: composite
33+
steps:
34+
- name: Classify changed files
35+
id: classify
36+
shell: bash
37+
env:
38+
GH_TOKEN: ${{ github.token }}
39+
REPO: ${{ github.repository }}
40+
EVENT_NAME: ${{ github.event_name }}
41+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
42+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
43+
run: |
44+
set -euo pipefail
45+
46+
# Only pull_request events are gated. Other events (push, release,
47+
# dispatch) leave CHANGED empty, so the classifier fails open and every
48+
# lane runs. Post-merge / on-demand validation is never weakened.
49+
if [ "$EVENT_NAME" = "pull_request" ]; then
50+
# Use the compare endpoint with the pinned base/head SHAs from the
51+
# event payload instead of the "current PR files" endpoint. The SHAs
52+
# are frozen at trigger time, so the file list is deterministic even
53+
# if the PR receives a new push between trigger and detect.
54+
CHANGED="$(gh api \
55+
--paginate \
56+
"repos/${REPO}/compare/${BASE_SHA}...${HEAD_SHA}" \
57+
--jq '.files[].filename' || true)"
58+
fi
59+
60+
echo "Changed files:"
61+
printf '%s\n' "${CHANGED:-(none)}"
62+
printf '%s\n' "${CHANGED:-}" | python3 scripts/ci/classify_changes.py

.github/actions/retry/action.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Retry a flaky command
2+
description: >-
3+
Run a shell command, retrying on non-zero exit. For dependency installs
4+
(npm ci, uv sync) whose only failures are transient network/toolchain
5+
flakes — a node-gyp header fetch, a registry blip — so CI self-heals
6+
instead of needing a manual re-run.
7+
8+
inputs:
9+
command:
10+
description: Shell command to run (and retry).
11+
required: true
12+
attempts:
13+
description: Max attempts before giving up.
14+
default: "3"
15+
delay:
16+
description: Seconds to wait between attempts.
17+
default: "10"
18+
working-directory:
19+
description: Directory to run in.
20+
default: "."
21+
22+
runs:
23+
using: composite
24+
steps:
25+
- shell: bash
26+
working-directory: ${{ inputs.working-directory }}
27+
# command goes through env, never interpolated into the script body, so
28+
# a command with quotes/specials can't break or inject into the runner.
29+
env:
30+
_CMD: ${{ inputs.command }}
31+
_ATTEMPTS: ${{ inputs.attempts }}
32+
_DELAY: ${{ inputs.delay }}
33+
run: |
34+
set -uo pipefail
35+
n=0
36+
while :; do
37+
n=$((n + 1))
38+
echo "::group::attempt $n/$_ATTEMPTS: $_CMD"
39+
if bash -c "$_CMD"; then
40+
echo "::endgroup::"
41+
exit 0
42+
fi
43+
echo "::endgroup::"
44+
if [ "$n" -ge "$_ATTEMPTS" ]; then
45+
echo "::error::failed after $n attempts: $_CMD"
46+
exit 1
47+
fi
48+
echo "::warning::attempt $n failed; retrying in ${_DELAY}s: $_CMD"
49+
sleep "$_DELAY"
50+
done

.github/workflows/build-windows-installer.yml

Lines changed: 0 additions & 100 deletions
This file was deleted.

.github/workflows/ci.yml

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
name: CI
2+
3+
# Orchestrator workflow. Runs ``detect-changes`` once, then conditionally
4+
# calls the sub-workflows that a PR can actually affect. A final
5+
# ``all-checks-pass`` gate job aggregates results so branch protection only
6+
# needs to require a single check.
7+
#
8+
# Sub-workflows are triggered via ``workflow_call`` and keep their own job
9+
# definitions, matrices, and concurrency settings. They no longer have
10+
# ``push:`` / ``pull_request:`` triggers of their own — everything flows
11+
# through this file.
12+
13+
on:
14+
pull_request:
15+
branches: [main]
16+
push:
17+
branches: [main]
18+
19+
permissions:
20+
contents: read
21+
pull-requests: write # needed by lint (PR comment) + supply-chain (PR comment)
22+
actions: read # needed by osv-scanner (SARIF upload)
23+
security-events: write # needed by osv-scanner (SARIF upload)
24+
25+
concurrency:
26+
group: ci-${{ github.ref }}
27+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
28+
29+
jobs:
30+
# ─────────────────────────────────────────────────────────────────────
31+
# detect: run the classifier once. Every downstream job reads its outputs
32+
# to decide whether to run. On push/dispatch the classifier fails open
33+
# (all lanes true) so post-merge validation is never weakened.
34+
# ─────────────────────────────────────────────────────────────────────
35+
detect:
36+
runs-on: ubuntu-latest
37+
outputs:
38+
python: ${{ steps.classify.outputs.python }}
39+
frontend: ${{ steps.classify.outputs.frontend }}
40+
site: ${{ steps.classify.outputs.site }}
41+
scan: ${{ steps.classify.outputs.scan }}
42+
deps: ${{ steps.classify.outputs.deps }}
43+
docker_meta: ${{ steps.classify.outputs.docker_meta }}
44+
mcp_catalog: ${{ steps.classify.outputs.mcp_catalog }}
45+
event_name: ${{ github.event_name }}
46+
steps:
47+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
48+
- name: Detect affected areas
49+
id: classify
50+
uses: ./.github/actions/detect-changes
51+
52+
# ─────────────────────────────────────────────────────────────────────
53+
# Lane-gated sub-workflows. Each runs in parallel after detect finishes.
54+
# Skipped workflows (if condition is false) don't spin up runners.
55+
# ─────────────────────────────────────────────────────────────────────
56+
tests:
57+
needs: detect
58+
if: needs.detect.outputs.python == 'true'
59+
uses: ./.github/workflows/tests.yml
60+
61+
lint:
62+
needs: detect
63+
if: needs.detect.outputs.python == 'true'
64+
uses: ./.github/workflows/lint.yml
65+
with:
66+
event_name: ${{ needs.detect.outputs.event_name }}
67+
68+
typecheck:
69+
needs: detect
70+
if: needs.detect.outputs.frontend == 'true'
71+
uses: ./.github/workflows/typecheck.yml
72+
73+
docs-site:
74+
needs: detect
75+
if: needs.detect.outputs.site == 'true'
76+
uses: ./.github/workflows/docs-site-checks.yml
77+
78+
history-check:
79+
needs: detect
80+
if: needs.detect.outputs.event_name == 'pull_request'
81+
uses: ./.github/workflows/history-check.yml
82+
83+
contributor-check:
84+
needs: detect
85+
if: needs.detect.outputs.python == 'true'
86+
uses: ./.github/workflows/contributor-check.yml
87+
88+
uv-lockfile:
89+
needs: detect
90+
uses: ./.github/workflows/uv-lockfile-check.yml
91+
92+
docker-lint:
93+
needs: detect
94+
if: needs.detect.outputs.docker_meta == 'true'
95+
uses: ./.github/workflows/docker-lint.yml
96+
97+
supply-chain:
98+
needs: detect
99+
if: needs.detect.outputs.event_name == 'pull_request' && (needs.detect.outputs.scan == 'true' || needs.detect.outputs.deps == 'true' || needs.detect.outputs.mcp_catalog == 'true')
100+
uses: ./.github/workflows/supply-chain-audit.yml
101+
with:
102+
event_name: ${{ needs.detect.outputs.event_name }}
103+
scan: ${{ needs.detect.outputs.scan == 'true' }}
104+
deps: ${{ needs.detect.outputs.deps == 'true' }}
105+
mcp_catalog: ${{ needs.detect.outputs.mcp_catalog == 'true' }}
106+
107+
osv-scanner:
108+
needs: detect
109+
uses: ./.github/workflows/osv-scanner.yml
110+
111+
# ─────────────────────────────────────────────────────────────────────
112+
# Gate: runs after everything. ``if: always()`` ensures it reports a
113+
# status even when some deps were skipped. Only actual ``failure``
114+
# results cause it to fail; ``skipped`` is treated as success.
115+
#
116+
# Branch protection should require ONLY this check.
117+
# ─────────────────────────────────────────────────────────────────────
118+
all-checks-pass:
119+
name: All required checks pass
120+
needs:
121+
- tests
122+
- lint
123+
- typecheck
124+
- docs-site
125+
- history-check
126+
- contributor-check
127+
- uv-lockfile
128+
- docker-lint
129+
- supply-chain
130+
- osv-scanner
131+
if: always()
132+
runs-on: ubuntu-latest
133+
steps:
134+
- name: Evaluate job results
135+
env:
136+
RESULTS: ${{ toJSON(needs.*.result) }}
137+
run: |
138+
echo "$RESULTS" | python3 -c "
139+
import json, sys
140+
results = json.load(sys.stdin)
141+
failed = [r for r in results if r == 'failure']
142+
if failed:
143+
print(f'::error::{len(failed)} job(s) failed')
144+
sys.exit(1)
145+
print('All checks passed (or were skipped)')
146+
"

.github/workflows/contributor-check.yml

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
name: Contributor Attribution Check
22

33
on:
4-
# No paths filter — the job must always run so the required check
5-
# reports a status (path-gated workflows leave checks "pending" forever
6-
# when no matching files change, which blocks merge).
7-
pull_request:
8-
branches: [main]
4+
workflow_call:
5+
96
permissions:
107
contents: read
118

@@ -17,21 +14,7 @@ jobs:
1714
with:
1815
fetch-depth: 0 # Full history needed for git log
1916

20-
- name: Check if relevant files changed
21-
id: filter
22-
run: |
23-
BASE="${{ github.event.pull_request.base.sha }}"
24-
HEAD="${{ github.event.pull_request.head.sha }}"
25-
CHANGED=$(git diff --name-only "$BASE"..."$HEAD" -- '*.py' '**/*.py' '.github/workflows/contributor-check.yml' || true)
26-
if [ -n "$CHANGED" ]; then
27-
echo "run=true" >> "$GITHUB_OUTPUT"
28-
else
29-
echo "run=false" >> "$GITHUB_OUTPUT"
30-
echo "No Python files changed, skipping attribution check."
31-
fi
32-
3317
- name: Check for unmapped contributor emails
34-
if: steps.filter.outputs.run == 'true'
3518
run: |
3619
# Get the merge base between this PR and main
3720
MERGE_BASE=$(git merge-base origin/main HEAD)

0 commit comments

Comments
 (0)