diff --git a/.github/actions/ensure-base-commit/action.yml b/.github/actions/ensure-base-commit/action.yml index 0f87bce112d..5a66566e871 100644 --- a/.github/actions/ensure-base-commit/action.yml +++ b/.github/actions/ensure-base-commit/action.yml @@ -23,6 +23,16 @@ runs: exit 0 fi + if ! [[ "$BASE_SHA" =~ ^[0-9a-fA-F]{7,40}$ ]]; then + echo "::error title=ensure-base-commit invalid base sha::Refusing invalid base SHA: $BASE_SHA" + exit 2 + fi + + if ! git check-ref-format --branch "$FETCH_REF" >/dev/null 2>&1; then + echo "::error title=ensure-base-commit invalid fetch ref::Refusing invalid fetch ref: $FETCH_REF" + exit 2 + fi + if git rev-parse --verify "$BASE_SHA^{commit}" >/dev/null 2>&1; then echo "Base commit already present: $BASE_SHA" exit 0 @@ -30,7 +40,7 @@ runs: for deepen_by in 25 100 300; do echo "Base commit missing; deepening $FETCH_REF by $deepen_by." - if ! git fetch --no-tags --deepen="$deepen_by" origin "$FETCH_REF"; then + if ! git fetch --no-tags --deepen="$deepen_by" origin -- "$FETCH_REF"; then echo "::warning title=ensure-base-commit fetch failed::Failed to deepen $FETCH_REF by $deepen_by while looking for $BASE_SHA" fi if git rev-parse --verify "$BASE_SHA^{commit}" >/dev/null 2>&1; then @@ -40,7 +50,7 @@ runs: done echo "Base commit still missing; fetching full history for $FETCH_REF." - if ! git fetch --no-tags origin "$FETCH_REF"; then + if ! git fetch --no-tags origin -- "$FETCH_REF"; then echo "::warning title=ensure-base-commit fetch failed::Failed to fetch full history for $FETCH_REF while looking for $BASE_SHA" fi if git rev-parse --verify "$BASE_SHA^{commit}" >/dev/null 2>&1; then diff --git a/.github/workflows/auto-response.yml b/.github/workflows/auto-response.yml index f6c527d1e37..15e31000b24 100644 --- a/.github/workflows/auto-response.yml +++ b/.github/workflows/auto-response.yml @@ -11,6 +11,10 @@ on: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request_target' }} + permissions: {} jobs: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94a15639074..a08c4f3eb11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,22 +6,24 @@ on: pull_request: types: [opened, reopened, synchronize, ready_for_review, converted_to_draft] +permissions: + contents: read + concurrency: - group: ${{ github.event_name == 'pull_request' && format('ci-pr-{0}', github.event.pull_request.number) || format('ci-push-{0}', github.run_id) }} + group: ${{ github.event_name == 'pull_request' && format('{0}-{1}', github.workflow, github.event.pull_request.number) || format('{0}-{1}', github.workflow, github.run_id) }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" jobs: - # Preflight: establish the fast global truth for this revision before the - # expensive platform and test lanes fan out. + # Scope: establish the fast global truth for this revision before the + # expensive platform and platform-specific lanes fan out. # Detect docs-only changes to skip heavy jobs (test, build, Windows, macOS, Android). - # Run scope detection, changed-extension detection, and fast security checks in - # one visible job so operators have a single preflight box to inspect and rerun. + # Keep this job focused on routing decisions so the rest of CI can fan out sooner. # Fail-safe: if detection steps are skipped, downstream outputs fall back to # conservative defaults that keep heavy lanes enabled. - preflight: + scope: if: github.event_name != 'pull_request' || !github.event.pull_request.draft runs-on: blacksmith-16vcpu-ubuntu-2404 timeout-minutes: 20 @@ -41,6 +43,7 @@ jobs: with: fetch-depth: 1 fetch-tags: false + persist-credentials: false submodules: false - name: Ensure preflight base commit @@ -101,6 +104,46 @@ jobs: appendFileSync(process.env.GITHUB_OUTPUT, `changed_extensions_matrix=${matrix}\n`, "utf8"); EOF + # Run the fast security/SCM checks in parallel with scope detection so the + # main Node jobs do not have to wait for Python/pre-commit setup. + security-fast: + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + runs-on: blacksmith-16vcpu-ubuntu-2404 + timeout-minutes: 20 + env: + PRE_COMMIT_CACHE_KEY_SUFFIX: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.sha }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 1 + fetch-tags: false + persist-credentials: false + submodules: false + + - name: Ensure security base commit + uses: ./.github/actions/ensure-base-commit + with: + base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }} + fetch-ref: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }} + + - name: Prepare trusted pre-commit config + if: github.event_name == 'pull_request' + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + set -euo pipefail + trusted_config="$RUNNER_TEMP/pre-commit-base.yaml" + git show "${BASE_SHA}:.pre-commit-config.yaml" > "$trusted_config" + echo "PRE_COMMIT_CONFIG_PATH=$trusted_config" >> "$GITHUB_ENV" + + - name: Setup Node environment + uses: ./.github/actions/setup-node-env + with: + install-bun: "false" + install-deps: "false" + use-sticky-disk: "false" + - name: Setup Python id: setup-python uses: actions/setup-python@v6 @@ -116,15 +159,17 @@ jobs: uses: actions/cache@v5 with: path: ~/.cache/pre-commit - key: pre-commit-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }} + key: pre-commit-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }}-${{ env.PRE_COMMIT_CACHE_KEY_SUFFIX }} + restore-keys: | + pre-commit-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }}- - name: Install pre-commit run: | python -m pip install --upgrade pip - python -m pip install pre-commit + python -m pip install pre-commit==4.2.0 - name: Detect committed private keys - run: pre-commit run --all-files detect-private-key + run: pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" --all-files detect-private-key - name: Audit changed GitHub workflows with zizmor env: @@ -151,24 +196,24 @@ jobs: fi printf 'Auditing workflow files:\n%s\n' "${workflow_files[@]}" - pre-commit run zizmor --files "${workflow_files[@]}" + pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" zizmor --files "${workflow_files[@]}" - name: Audit production dependencies - if: steps.docs_scope.outputs.docs_only != 'true' - run: pre-commit run --all-files pnpm-audit-prod + run: pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" --all-files pnpm-audit-prod # Fanout: downstream lanes branch from preflight outputs instead of waiting # on unrelated Linux checks. # Build dist once for Node-relevant changes and share it with downstream jobs. build-artifacts: - needs: [preflight] - if: needs.preflight.outputs.docs_only != 'true' && needs.preflight.outputs.run_node == 'true' + needs: [scope] + if: needs.scope.outputs.docs_only != 'true' && needs.scope.outputs.run_node == 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@v6 with: + persist-credentials: false submodules: false - name: Ensure secrets base commit (PR fast path) @@ -207,14 +252,15 @@ jobs: # Validate npm pack contents after build (only on push to main, not PRs). release-check: - needs: [preflight, build-artifacts] - if: github.event_name == 'push' && needs.preflight.outputs.docs_only != 'true' + needs: [scope, build-artifacts] + if: github.event_name == 'push' && needs.scope.outputs.docs_only != 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@v6 with: + persist-credentials: false submodules: false - name: Setup Node environment @@ -232,9 +278,42 @@ jobs: - name: Check release contents run: pnpm release:check + checks-fast: + needs: [scope] + if: always() && needs.scope.outputs.docs_only != 'true' && needs.scope.outputs.run_node == 'true' + runs-on: blacksmith-16vcpu-ubuntu-2404 + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + - runtime: node + task: extensions + command: pnpm test:extensions + - runtime: node + task: contracts-protocol + command: | + pnpm test:contracts + pnpm protocol:check + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + submodules: false + + - name: Setup Node environment + uses: ./.github/actions/setup-node-env + with: + install-bun: "false" + use-sticky-disk: "false" + + - name: Run ${{ matrix.task }} (${{ matrix.runtime }}) + run: ${{ matrix.command }} + checks: - needs: [preflight, build-artifacts] - if: always() && needs.preflight.outputs.docs_only != 'true' && needs.preflight.outputs.run_node == 'true' && needs.build-artifacts.result == 'success' + needs: [scope, build-artifacts] + if: always() && needs.scope.outputs.docs_only != 'true' && needs.scope.outputs.run_node == 'true' && needs.build-artifacts.result == 'success' runs-on: blacksmith-16vcpu-ubuntu-2404 timeout-minutes: 20 strategy: @@ -251,17 +330,11 @@ jobs: shard_index: 2 shard_count: 2 command: pnpm test - - runtime: node - task: extensions - command: pnpm test:extensions - runtime: node task: channels shard_index: 1 shard_count: 3 command: pnpm test:channels - - runtime: node - task: contracts - command: pnpm test:contracts - runtime: node task: channels shard_index: 2 @@ -272,9 +345,6 @@ jobs: shard_index: 3 shard_count: 3 command: pnpm test:channels - - runtime: node - task: protocol - command: pnpm protocol:check - runtime: node task: compat-node22 node_version: "22.x" @@ -296,6 +366,7 @@ jobs: if: github.event_name != 'pull_request' || matrix.task != 'compat-node22' uses: actions/checkout@v6 with: + persist-credentials: false submodules: false - name: Setup Node environment @@ -346,17 +417,18 @@ jobs: extension-fast: name: "extension-fast" - needs: [preflight] - if: needs.preflight.outputs.docs_only != 'true' && needs.preflight.outputs.run_node == 'true' && needs.preflight.outputs.has_changed_extensions == 'true' + needs: [scope] + if: needs.scope.outputs.docs_only != 'true' && needs.scope.outputs.run_node == 'true' && needs.scope.outputs.has_changed_extensions == 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 timeout-minutes: 20 strategy: fail-fast: false - matrix: ${{ fromJson(needs.preflight.outputs.changed_extensions_matrix) }} + matrix: ${{ fromJson(needs.scope.outputs.changed_extensions_matrix) }} steps: - name: Checkout uses: actions/checkout@v6 with: + persist-credentials: false submodules: false - name: Setup Node environment @@ -373,14 +445,15 @@ jobs: # Types, lint, and format check. check: name: "check" - needs: [preflight] - if: needs.preflight.outputs.docs_only != 'true' && needs.preflight.outputs.run_node == 'true' + needs: [scope] + if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && needs.scope.outputs.docs_only != 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@v6 with: + persist-credentials: false submodules: false - name: Setup Node environment @@ -397,14 +470,15 @@ jobs: check-additional: name: "check-additional" - needs: [preflight] - if: needs.preflight.outputs.docs_only != 'true' && needs.preflight.outputs.run_node == 'true' + needs: [scope] + if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && needs.scope.outputs.docs_only != 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@v6 with: + persist-credentials: false submodules: false - name: Setup Node environment @@ -495,14 +569,15 @@ jobs: build-smoke: name: "build-smoke" - needs: [preflight, build-artifacts] - if: always() && needs.preflight.outputs.docs_only != 'true' && needs.preflight.outputs.run_node == 'true' && (github.event_name != 'push' || needs.build-artifacts.result == 'success') + needs: [scope, build-artifacts] + if: always() && needs.scope.outputs.docs_only != 'true' && needs.scope.outputs.run_node == 'true' && (github.event_name != 'push' || needs.build-artifacts.result == 'success') runs-on: blacksmith-16vcpu-ubuntu-2404 timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@v6 with: + persist-credentials: false submodules: false - name: Setup Node environment @@ -536,14 +611,15 @@ jobs: # Validate docs (format, lint, broken links) only when docs files changed. check-docs: - needs: [preflight] - if: needs.preflight.outputs.docs_changed == 'true' + needs: [scope] + if: needs.scope.outputs.docs_changed == 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@v6 with: + persist-credentials: false submodules: false - name: Setup Node environment @@ -556,14 +632,15 @@ jobs: run: pnpm check:docs skills-python: - needs: [preflight] - if: needs.preflight.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.preflight.outputs.run_skills_python == 'true') + needs: [scope] + if: needs.scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.scope.outputs.run_skills_python == 'true') runs-on: blacksmith-16vcpu-ubuntu-2404 timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@v6 with: + persist-credentials: false submodules: false - name: Setup Python @@ -583,8 +660,8 @@ jobs: run: python -m pytest -q skills checks-windows: - needs: [preflight, build-artifacts] - if: always() && needs.preflight.outputs.docs_only != 'true' && needs.preflight.outputs.run_windows == 'true' && needs.build-artifacts.result == 'success' + needs: [scope, build-artifacts] + if: always() && needs.scope.outputs.docs_only != 'true' && needs.scope.outputs.run_windows == 'true' && needs.build-artifacts.result == 'success' runs-on: blacksmith-32vcpu-windows-2025 timeout-minutes: 20 env: @@ -643,6 +720,7 @@ jobs: - name: Checkout uses: actions/checkout@v6 with: + persist-credentials: false submodules: false - name: Try to exclude workspace from Windows Defender (best-effort) @@ -732,14 +810,15 @@ jobs: # running 4 separate jobs per PR (as before) starved the queue. One job # per PR allows 5 PRs to run macOS checks simultaneously. macos: - needs: [preflight] - if: github.event_name == 'pull_request' && needs.preflight.outputs.docs_only != 'true' && needs.preflight.outputs.run_macos == 'true' + needs: [scope] + if: github.event_name == 'pull_request' && needs.scope.outputs.docs_only != 'true' && needs.scope.outputs.run_macos == 'true' runs-on: macos-latest timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@v6 with: + persist-credentials: false submodules: false - name: Setup Node environment @@ -809,8 +888,8 @@ jobs: exit 1 android: - needs: [preflight] - if: needs.preflight.outputs.docs_only != 'true' && needs.preflight.outputs.run_android == 'true' + needs: [scope] + if: needs.scope.outputs.docs_only != 'true' && needs.scope.outputs.run_android == 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 timeout-minutes: 20 strategy: @@ -829,6 +908,7 @@ jobs: - name: Checkout uses: actions/checkout@v6 with: + persist-credentials: false submodules: false - name: Setup Java @@ -858,7 +938,7 @@ jobs: echo "$ANDROID_SDK_ROOT/platform-tools" >> "$GITHUB_PATH" - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5 with: gradle-version: 8.11.1 diff --git a/.github/workflows/install-smoke.yml b/.github/workflows/install-smoke.yml index 01b4eae0a31..200ce7595d7 100644 --- a/.github/workflows/install-smoke.yml +++ b/.github/workflows/install-smoke.yml @@ -8,7 +8,7 @@ on: workflow_dispatch: concurrency: - group: ${{ github.event_name == 'pull_request' && format('install-smoke-pr-{0}', github.event.pull_request.number) || format('install-smoke-push-{0}', github.run_id) }} + group: ${{ github.event_name == 'pull_request' && format('{0}-{1}', github.workflow, github.event.pull_request.number) || format('{0}-{1}', github.workflow, github.run_id) }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 3a38e5213c3..143ebe4025e 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -19,6 +19,10 @@ on: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request_target' }} + permissions: {} jobs: diff --git a/.github/workflows/sandbox-common-smoke.yml b/.github/workflows/sandbox-common-smoke.yml index 7eac64746f9..d5f560af466 100644 --- a/.github/workflows/sandbox-common-smoke.yml +++ b/.github/workflows/sandbox-common-smoke.yml @@ -15,7 +15,7 @@ on: - scripts/sandbox-common-setup.sh concurrency: - group: sandbox-common-smoke-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: diff --git a/.github/workflows/workflow-sanity.yml b/.github/workflows/workflow-sanity.yml index 5c6bee04a14..e1a16fe977e 100644 --- a/.github/workflows/workflow-sanity.yml +++ b/.github/workflows/workflow-sanity.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: concurrency: - group: workflow-sanity-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: