From c3d852eac2311e6827ca4924d394ca688b66edd1 Mon Sep 17 00:00:00 2001 From: aalsanie Date: Mon, 2 Nov 2026 11:09:33 +0300 Subject: [PATCH 1/8] ci: gate verification with required checks --- .github/workflows/ci.yml | 122 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 115 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f5c4c7..00ff936 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: branches: [main] permissions: + actions: read contents: read concurrency: @@ -14,21 +15,64 @@ concurrency: jobs: matrix: - name: CI matrix policy + name: Preflight runs-on: ubuntu-latest timeout-minutes: 5 outputs: + expensive: ${{ steps.changes.outputs.expensive }} runtime_jdks: ${{ steps.matrix.outputs.runtime_jdks }} kotlin: ${{ steps.matrix.outputs.kotlin }} os: ${{ steps.matrix.outputs.os }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: + fetch-depth: 0 persist-credentials: false - name: Verify compatibility policy run: python scripts/verify-compatibility-matrix.py + - name: Classify changes + id: changes + shell: bash + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + python - <<'PY' + import os + import subprocess + + expensive = True + if os.environ["GITHUB_EVENT_NAME"] == "pull_request": + base, head = os.environ["BASE_SHA"], os.environ["HEAD_SHA"] + changed = subprocess.check_output([ + "git", "diff", "--no-renames", "--name-only", "-z", f"{base}...{head}" + ]) + paths = [p for p in changed.decode("utf-8", "surrogateescape").split("\0") if p] + def trivial(path): + return ( + path in ("README.md", "CHANGELOG.md") + or (path.startswith("docs/") and path.endswith((".md", ".png", ".jpg", ".jpeg", ".svg", ".webp"))) + or (path.startswith(".github/ISSUE_TEMPLATE/") and path.endswith(".md")) + ) + expensive = not (paths and all(map(trivial, paths))) + subprocess.run(["git", "diff", "--check", f"{base}...{head}"], check=True) + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as out: + out.write(f"expensive={str(expensive).lower()}\n") + PY + + - name: Verify required CI approver + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + gh api "repos/${GITHUB_REPOSITORY}/environments/ci-approved" --jq \ + 'if .can_admins_bypass == false then [.protection_rules[] | select(.type == "required_reviewers" and .prevent_self_review == false and (.reviewers | length) == 1 and .reviewers[0].type == "User" and .reviewers[0].reviewer.id == 22101794)] | length else 0 end' \ + | grep -qx '1' + - id: matrix name: Export required check matrices shell: bash @@ -51,8 +95,20 @@ jobs: })) PY + authorize: + name: Approve expensive CI + needs: matrix + if: needs.matrix.outputs.expensive == 'true' + runs-on: ubuntu-latest + environment: + name: ci-approved + deployment: false + steps: + - run: echo "CI approved" + verify: - needs: [matrix] + needs: [matrix, authorize] + if: needs.authorize.result == 'success' runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -114,7 +170,8 @@ jobs: retention-days: 14 kotlin-consumers: - needs: [matrix] + needs: [matrix, authorize] + if: needs.authorize.result == 'success' runs-on: ubuntu-latest timeout-minutes: 25 strategy: @@ -144,8 +201,8 @@ jobs: kotlin-consumers-gate: name: Kotlin consumers gate - if: always() - needs: [matrix, kotlin-consumers] + if: always() && needs.authorize.result == 'success' + needs: [matrix, authorize, kotlin-consumers] runs-on: ubuntu-latest timeout-minutes: 5 steps: @@ -160,7 +217,8 @@ jobs: test "$KOTLIN_RESULT" = "success" runtime-jdks: - needs: [matrix] + needs: [matrix, authorize] + if: needs.authorize.result == 'success' runs-on: ubuntu-latest timeout-minutes: 20 strategy: @@ -193,7 +251,8 @@ jobs: --stacktrace operating-systems: - needs: [matrix] + needs: [matrix, authorize] + if: needs.authorize.result == 'success' runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: @@ -239,3 +298,52 @@ jobs: .\gradlew.bat -p smoke-test-kotlin "-Dmaven.repo.local=$repo" clean check --stacktrace if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + compatibility: + name: Compatibility + needs: authorize + if: needs.authorize.result == 'success' + uses: ./.github/workflows/compatibility.yml + permissions: + contents: read + + codeql: + name: CodeQL + needs: authorize + if: needs.authorize.result == 'success' + uses: ./.github/workflows/codeql.yml + permissions: + contents: read + security-events: write + + required: + name: Required checks + if: always() + needs: [matrix, authorize, verify, kotlin-consumers, kotlin-consumers-gate, runtime-jdks, operating-systems, compatibility, codeql] + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Verify required jobs + shell: bash + env: + NEEDS_JSON: ${{ toJSON(needs) }} + run: | + python - <<'PY' + import json + import os + import sys + + needs = json.loads(os.environ["NEEDS_JSON"]) + results = {name: job["result"] for name, job in needs.items()} + if results["matrix"] != "success": + sys.exit("Preflight failed") + expensive = needs["matrix"]["outputs"].get("expensive") + if expensive not in ("true", "false"): + sys.exit("Invalid change classification") + expected = "success" if expensive == "true" else "skipped" + for name in ("authorize", "verify", "kotlin-consumers", "kotlin-consumers-gate", + "runtime-jdks", "operating-systems", "compatibility", "codeql"): + if results[name] != expected: + sys.exit(f"{name}: expected {expected}, got {results[name]}") + print("All applicable checks passed") + PY From 5b3cc8db05006cf0bc76def671fe642a77759b85 Mon Sep 17 00:00:00 2001 From: aalsanie Date: Mon, 2 Nov 2026 11:09:35 +0300 Subject: [PATCH 2/8] ci: run compatibility on demand or from CI --- .github/workflows/compatibility.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 5c20fdd..caded7f 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -1,9 +1,8 @@ name: Compatibility on: - pull_request: - push: - branches: [main] + workflow_call: + workflow_dispatch: permissions: contents: read From ea3c10b76928927ac2c686695935f767b0992f79 Mon Sep 17 00:00:00 2001 From: aalsanie Date: Mon, 2 Nov 2026 11:09:39 +0300 Subject: [PATCH 3/8] ci: run CodeQL on demand or from CI --- .github/workflows/codeql.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ce2533c..1e427a5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,11 +1,8 @@ name: CodeQL on: - pull_request: - push: - branches: [main] - schedule: - - cron: "23 4 * * 1" + workflow_call: + workflow_dispatch: permissions: contents: read From 90020a90e126c1bc246e01e81fb1971aa9d5de1a Mon Sep 17 00:00:00 2001 From: aalsanie Date: Mon, 2 Nov 2026 11:09:41 +0300 Subject: [PATCH 4/8] ci: submit dependencies on demand --- .github/workflows/dependency-submission.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml index 9b26e48..10b6017 100644 --- a/.github/workflows/dependency-submission.yml +++ b/.github/workflows/dependency-submission.yml @@ -1,8 +1,6 @@ name: Dependency Submission on: - push: - branches: [main] workflow_dispatch: permissions: From d1b6a7ead53cc4e66a36529415347b21b9d0bb40 Mon Sep 17 00:00:00 2001 From: aalsanie Date: Mon, 2 Nov 2026 11:09:44 +0300 Subject: [PATCH 5/8] ci: require approval before release --- .github/workflows/release.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d11c949..ebb8582 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,7 @@ on: - "!v*.*.*-*" permissions: + actions: read contents: read concurrency: @@ -14,7 +15,33 @@ concurrency: cancel-in-progress: false jobs: + preflight: + name: Release preflight + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Verify required CI approver + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + gh api "repos/${GITHUB_REPOSITORY}/environments/ci-approved" --jq \ + 'if .can_admins_bypass == false then [.protection_rules[] | select(.type == "required_reviewers" and .prevent_self_review == false and (.reviewers | length) == 1 and .reviewers[0].type == "User" and .reviewers[0].reviewer.id == 22101794)] | length else 0 end' \ + | grep -qx '1' + + authorize: + name: Approve release CI + needs: preflight + runs-on: ubuntu-latest + environment: + name: ci-approved + deployment: false + steps: + - run: echo "Release CI approved" + publish: + needs: authorize runs-on: ubuntu-latest timeout-minutes: 90 From 0d818b78e553de35789a511d6b778f0da2db1388 Mon Sep 17 00:00:00 2001 From: aalsanie Date: Mon, 2 Nov 2026 11:12:16 +0300 Subject: [PATCH 6/8] ci: validate Gradle wrapper in preflight --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00ff936..6633944 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,8 @@ jobs: fetch-depth: 0 persist-credentials: false + - uses: gradle/actions/wrapper-validation@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 + - name: Verify compatibility policy run: python scripts/verify-compatibility-matrix.py From 5657317c8e2f5b7e62f2c316ddfece29ac16d9d0 Mon Sep 17 00:00:00 2001 From: aalsanie Date: Mon, 2 Nov 2026 11:45:05 +0300 Subject: [PATCH 7/8] ci: force CodeQL Java compilation --- .github/workflows/codeql.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1e427a5..0fce1fe 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,6 +41,10 @@ jobs: :testClasses :codes-spring:testClasses :reference-spring-orders:testClasses + --rerun-tasks + --no-build-cache + --no-configuration-cache + --no-daemon --stacktrace - uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 From 74f7de2ad74a0e597bbd52e1f77ab64a8287ecd0 Mon Sep 17 00:00:00 2001 From: aalsanie Date: Mon, 2 Nov 2026 14:54:21 +0300 Subject: [PATCH 8/8] ci: preserve standalone CodeQL analysis configuration --- .github/workflows/ci.yml | 13 ++----------- .github/workflows/codeql.yml | 7 +++++-- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6633944..f56a0f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -309,19 +309,10 @@ jobs: permissions: contents: read - codeql: - name: CodeQL - needs: authorize - if: needs.authorize.result == 'success' - uses: ./.github/workflows/codeql.yml - permissions: - contents: read - security-events: write - required: name: Required checks if: always() - needs: [matrix, authorize, verify, kotlin-consumers, kotlin-consumers-gate, runtime-jdks, operating-systems, compatibility, codeql] + needs: [matrix, authorize, verify, kotlin-consumers, kotlin-consumers-gate, runtime-jdks, operating-systems, compatibility] runs-on: ubuntu-latest timeout-minutes: 5 steps: @@ -344,7 +335,7 @@ jobs: sys.exit("Invalid change classification") expected = "success" if expensive == "true" else "skipped" for name in ("authorize", "verify", "kotlin-consumers", "kotlin-consumers-gate", - "runtime-jdks", "operating-systems", "compatibility", "codeql"): + "runtime-jdks", "operating-systems", "compatibility"): if results[name] != expected: sys.exit(f"{name}: expected {expected}, got {results[name]}") print("All applicable checks passed") diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0fce1fe..750a543 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,8 +1,11 @@ name: CodeQL on: - workflow_call: - workflow_dispatch: + pull_request: + push: + branches: [main] + schedule: + - cron: "23 4 * * 1" permissions: contents: read