From 47c3a0352f8190b9e203f60d8ffe0b13348eb298 Mon Sep 17 00:00:00 2001 From: Cheolmin Jo Date: Wed, 11 Nov 2026 11:31:06 +0900 Subject: [PATCH] ci: classify changes in the workflow and add a single CI gate check Replace paths-ignore, which leaves a required check pending on documentation-only pull requests, with a changes job and an always-running CI gate. The gate passes when the native matrix succeeded for a code change or was skipped for a documentation-only change, and fails otherwise. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 74 ++++++++++++++++++++++++++++++++++---- docs/guides/development.md | 13 ++++--- 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 797bd51..5d25c70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,16 +5,13 @@ name: CI # stage-1 executable, and stage 1 rebuilds a clean export (stage 2). Bootstrap test results never count for the # sources under test. See docs/guides/development.md#continuous-integration. +# Documentation-only changes (Markdown or docs/) skip the build matrix inside the workflow instead of through +# `paths-ignore`: a workflow that never starts leaves a required check pending forever and would block every +# documentation-only pull request. `CI gate` always runs and is the single required status check. on: pull_request: - paths-ignore: - - "**/*.md" - - "docs/**" push: branches: [main] - paths-ignore: - - "**/*.md" - - "docs/**" workflow_dispatch: permissions: @@ -25,8 +22,48 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + changes: + name: changes + runs-on: ubuntu-24.04 + timeout-minutes: 5 + outputs: + code: ${{ steps.classify.outputs.code }} + steps: + - name: Check out the history needed to classify the change + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Classify the change + id: classify + shell: bash + env: + EVENT: ${{ github.event_name }} + PR_BASE: ${{ github.event.pull_request.base.sha }} + PR_HEAD: ${{ github.event.pull_request.head.sha }} + PUSH_BEFORE: ${{ github.event.before }} + PUSH_HEAD: ${{ github.sha }} + run: | + set -euo pipefail + # Anything that cannot be classified runs the full matrix: skipping is only for a proven documentation-only change. + code=true + case "$EVENT" in + pull_request) range="$PR_BASE...$PR_HEAD" ;; + push) range="$PUSH_BEFORE..$PUSH_HEAD" ;; + *) range= ;; + esac + if [ -n "$range" ] && files="$(git diff --name-only "$range" 2>/dev/null)" && [ -n "$files" ]; then + if printf '%s\n' "$files" | grep -qvE '(\.md$|^docs/)'; then code=true; else code=false; fi + fi + echo "event=$EVENT range=${range:-none} code=$code" + printf '%s\n' "${files:-}" | sed 's/^/changed: /' + echo "code=$code" >> "$GITHUB_OUTPUT" + native: name: native (${{ matrix.label || matrix.host }}) + needs: changes + if: needs.changes.outputs.code == 'true' runs-on: ${{ matrix.runner }} timeout-minutes: 120 # Keep both old-LTS and new Linux loader coverage required after the producer's TLSDESC repair. @@ -122,3 +159,28 @@ jobs: ${{ runner.temp }}/crt-cli-ci/sdk-source.json if-no-files-found: warn retention-days: 14 + + # The only required status check. It always runs: a documentation-only change legitimately skips the matrix, but a + # code change whose matrix failed, was cancelled or never ran must not pass. + gate: + name: CI gate + needs: [changes, native] + if: ${{ always() }} + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Decide + shell: bash + env: + CHANGES: ${{ needs.changes.result }} + NATIVE: ${{ needs.native.result }} + CODE: ${{ needs.changes.outputs.code }} + run: | + set -euo pipefail + echo "changes=$CHANGES native=$NATIVE code=$CODE" + [ "$CHANGES" = success ] || { echo "change classification did not succeed"; exit 1; } + if [ "$CODE" = true ]; then + [ "$NATIVE" = success ] || { echo "code changed but the native matrix result is '$NATIVE'"; exit 1; } + else + [ "$NATIVE" = skipped ] || { echo "documentation-only change but the native matrix result is '$NATIVE'"; exit 1; } + fi diff --git a/docs/guides/development.md b/docs/guides/development.md index 65dc0c6..1929237 100644 --- a/docs/guides/development.md +++ b/docs/guides/development.md @@ -225,11 +225,14 @@ unsupported for programs that import a DLL beside them. repository permissions and no secrets. It builds nothing from CRT: it consumes published artifacts. The repository carries no executables (`bin/` was removed); CI bootstraps from a release. -Push and PR events use `paths-ignore: ["**/*.md", "docs/**"]`, following the -CRT workflow's documentation exclusions. A change consisting only of Markdown -or files under `docs/` skips automatic CI; a mixed documentation/code change -still runs. Sources, headers, tests, tools, manifests, CI pins and workflow -configuration remain covered. Manual dispatch is available regardless of paths. +Push and PR events always start the workflow. A `changes` job diffs the pull request (merge base to head) or the push +(`before..sha`) and sets `code=false` only when every changed file is Markdown or under `docs/`; anything it cannot classify +(new branch, unreachable range, manual dispatch) runs the full matrix. The build matrix is skipped for `code=false`. +`CI gate` always runs and is the one required status check: it passes when the matrix succeeded for a code change or was +skipped for a documentation-only change, and fails when the matrix failed, was cancelled or never ran. The workflow does +not use `paths-ignore`, because a workflow that never starts would leave a required check pending and block +documentation-only pull requests. Sources, headers, tests, tools, manifests, CI pins and workflow configuration remain +covered. Manual dispatch is available regardless of paths. 1. **Bootstrap.** `tools/ci_bootstrap.py` downloads the host's published CRT-CLI Preview and checks its size and SHA-256 against `.github/ci/pins.json`. The pin verifies byte identity;