Commit e6cc3e5f63 for openssl.org

commit e6cc3e5f63eb5035cefef5d715085f29bfc893d1
Author: Georgy Karataev <georgyk@openssl.org>
Date:   Tue Aug 4 11:52:15 2026 +0200

    ci: let the CI bot dispatch the platform, cross-compile, fuzzing and interop suites

    These five suites only run nightly, so a break in a platform, a cross-compiled
    target, a fuzzer or an interop peer is found the morning after it merged. Give
    each the dispatch entry point the daily workflows already have, and adapt the
    ones that assumed they only ever see their own default branch.

    os-zoo's self-hosted legs (ppc64le, s390x, riscv64) are restricted to the
    nightly cron and plain manual runs, keeping pull-request code off them; the
    GitHub-hosted legs gain the repository guard the rest of the file already had.
    The cross-compile suites' test steps, gated by event name, now also run on a
    dispatch instead of silently skipping. oss-fuzz checks out the requested tree
    so CIFuzz fuzzes it instead of the default branch. interop-tests skips
    openssh_interop on a dispatch, since it never checks this repo out.

    Refs: openssl/project#2027

    Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

    Reviewed-by: Milan Broz <mbroz@openssl.org>
    Reviewed-by: Norbert Pocs <norbertp@openssl.org>
    MergeDate: Fri Aug  7 13:31:12 2026
    (Merged from https://github.com/openssl/openssl/pull/32175)

diff --git a/.github/workflows/aarch64-more-cross-compiles.yml b/.github/workflows/aarch64-more-cross-compiles.yml
index 13fdef3925..1d0a3d5926 100644
--- a/.github/workflows/aarch64-more-cross-compiles.yml
+++ b/.github/workflows/aarch64-more-cross-compiles.yml
@@ -14,11 +14,42 @@ on:
   schedule:
     - cron: '05 03 * * *'
   workflow_dispatch:
+    inputs:
+      pr:
+        description: 'Internal: openssl-ci-bot PR number. Leave empty for a normal manual run.'
+        required: false
+        type: string
+      head_sha:
+        description: 'Internal: openssl-ci-bot commit SHA. Leave empty for a normal manual run.'
+        required: false
+        type: string
+      check_run_id:
+        description: 'Internal: openssl-ci-bot check-run ID. Leave empty for a normal manual run.'
+        required: false
+        type: string
+
+# Keep in sync with openssl-ci-bot's run-name parser, and with the Actions statistics
+# collector that attributes CI load by parsing this same string. Both break silently.
+run-name: >-
+  ${{ github.event.inputs.pr && format('ci-dispatch pr={0} head={1} check_run_id={2}', github.event.inputs.pr, github.event.inputs.head_sha, github.event.inputs.check_run_id) || github.workflow }}
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.inputs.pr || github.run_id }}
+  cancel-in-progress: true

 permissions:
   contents: read

 jobs:
+  # Only a dispatch carries inputs, so this is skipped on every other trigger.
+  validate-dispatch-inputs:
+    if: inputs.pr != '' || inputs.head_sha != '' || inputs.check_run_id != ''
+    uses: ./.github/workflows/validate-dispatch-inputs.yml
+    with:
+      pr: ${{ inputs.pr }}
+      head_sha: ${{ inputs.head_sha }}
+      check_run_id: ${{ inputs.check_run_id }}
+
   cross-compilation-aarch64:
     # pull request title contains 'aarch64'
     # pull request title contains 'arm64'
@@ -26,7 +57,11 @@ jobs:
     # push event commit message contains '[aarch64 ci]'
     # cron job
     # manual dispatch
-    if: contains(github.event.pull_request.title, 'aarch64') || contains(github.event.pull_request.title, 'AArch64') || contains(github.event.pull_request.title, 'arm64') || contains(github.event.pull_request.body, '[aarch64 ci]') || contains(github.event.head_commit.message, '[aarch64 ci]') || (github.event_name == 'schedule' && github.repository == 'openssl/openssl') || github.event_name == 'workflow_dispatch'
+    needs: [validate-dispatch-inputs]
+    if: >-
+      (contains(github.event.pull_request.title, 'aarch64') || contains(github.event.pull_request.title, 'AArch64') || contains(github.event.pull_request.title, 'arm64') || contains(github.event.pull_request.body, '[aarch64 ci]') || contains(github.event.head_commit.message, '[aarch64 ci]') || (github.event_name == 'schedule' && github.repository == 'openssl/openssl') || github.event_name == 'workflow_dispatch') &&
+      !cancelled() &&
+      (needs.validate-dispatch-inputs.result == 'success' || needs.validate-dispatch-inputs.result == 'skipped')
     strategy:
       fail-fast: false
       matrix:
@@ -134,6 +169,7 @@ jobs:
     - uses: actions/checkout@v6
       with:
         persist-credentials: false
+        ref: ${{ github.event.inputs.head_sha || github.sha }}
     - name: checkout fuzz/corpora submodule
       run: git submodule update --init --depth 1 fuzz/corpora

@@ -175,14 +211,17 @@ jobs:
       if: matrix.platform.tests != 'none'
       run: QEMU_LD_PREFIX=/usr/${{ matrix.platform.arch }} ./util/opensslwrap.sh info -cpusettings

+    # A dispatched run takes the push tier, not the pull_request one below: it exists to run
+    # pre-merge what currently only runs post-merge, and evp-only is the weaker signal. Legs
+    # setting `tests: none` stay build-only on every trigger, by design.
     - name: make all tests
-      if: github.event_name == 'push' && matrix.platform.tests == ''
+      if: (github.event_name == 'push' || inputs.pr != '') && matrix.platform.tests == ''
       run: |
         .github/workflows/make-test \
                   TESTS="-test_afalg" \
                   QEMU_LD_PREFIX=/usr/${{ matrix.platform.arch }}
     - name: make some tests
-      if: github.event_name == 'push' && matrix.platform.tests != 'none' && matrix.platform.tests != ''
+      if: (github.event_name == 'push' || inputs.pr != '') && matrix.platform.tests != 'none' && matrix.platform.tests != ''
       run: |
         .github/workflows/make-test \
                   TESTS="${{ matrix.platform.tests }} -test_afalg" \
diff --git a/.github/workflows/interop-tests.yml b/.github/workflows/interop-tests.yml
index 65e273f0e0..e9cc920d33 100644
--- a/.github/workflows/interop-tests.yml
+++ b/.github/workflows/interop-tests.yml
@@ -8,12 +8,51 @@ on:
   schedule:
     - cron: '55 02 * * *'
   workflow_dispatch:
+    inputs:
+      pr:
+        description: 'Internal: openssl-ci-bot PR number. Leave empty for a normal manual run.'
+        required: false
+        type: string
+      head_sha:
+        description: 'Internal: openssl-ci-bot commit SHA. Leave empty for a normal manual run.'
+        required: false
+        type: string
+      check_run_id:
+        description: 'Internal: openssl-ci-bot check-run ID. Leave empty for a normal manual run.'
+        required: false
+        type: string
+
+# Keep in sync with openssl-ci-bot's run-name parser, and with the Actions statistics
+# collector that attributes CI load by parsing this same string. Both break silently.
+run-name: >-
+  ${{ github.event.inputs.pr && format('ci-dispatch pr={0} head={1} check_run_id={2}', github.event.inputs.pr, github.event.inputs.head_sha, github.event.inputs.check_run_id) || github.workflow }}
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.inputs.pr || github.run_id }}
+  cancel-in-progress: true

 permissions: {}

 jobs:
+  # Only a dispatch carries inputs, so this is skipped on the nightly cron.
+  validate-dispatch-inputs:
+    if: |
+      github.repository == 'openssl/openssl' &&
+      (inputs.pr != '' || inputs.head_sha != '' || inputs.check_run_id != '')
+    uses: ./.github/workflows/validate-dispatch-inputs.yml
+    with:
+      pr: ${{ inputs.pr }}
+      head_sha: ${{ inputs.head_sha }}
+      check_run_id: ${{ inputs.check_run_id }}
+
   test:
-    if: github.repository == 'openssl/openssl'
+    needs: [validate-dispatch-inputs]
+    if: |
+      github.repository == 'openssl/openssl' &&
+      !cancelled() &&
+      (needs.validate-dispatch-inputs.result == 'success' || needs.validate-dispatch-inputs.result == 'skipped')
+    permissions:
+      contents: read
     runs-on: ubuntu-22.04
     container:
       image: docker.io/fedora:43
@@ -29,6 +68,7 @@ jobs:
       - uses: actions/checkout@v6
         with:
           persist-credentials: false
+          ref: ${{ github.event.inputs.head_sha || github.sha }}
       - name: Display environment
         run: export
       - name : Install needed tools
@@ -61,7 +101,12 @@ jobs:
           openssl version
           echo "Finished - important to prevent unwanted output truncating"
   openssh_interop:
-    if: github.repository == 'openssl/openssl'
+    needs: [validate-dispatch-inputs]
+    if: |
+      github.repository == 'openssl/openssl' &&
+      inputs.pr == '' &&
+      !cancelled() &&
+      (needs.validate-dispatch-inputs.result == 'success' || needs.validate-dispatch-inputs.result == 'skipped')
     name: "openssh interop ${{ matrix.branch.openssl }}"
     strategy:
       fail-fast: false
diff --git a/.github/workflows/os-zoo.yml b/.github/workflows/os-zoo.yml
index 1d8351b590..00421d23c4 100644
--- a/.github/workflows/os-zoo.yml
+++ b/.github/workflows/os-zoo.yml
@@ -11,13 +11,50 @@ on:
   schedule:
     - cron: '50 02 * * *'
   workflow_dispatch:
+    inputs:
+      pr:
+        description: 'Internal: openssl-ci-bot PR number. Leave empty for a normal manual run.'
+        required: false
+        type: string
+      head_sha:
+        description: 'Internal: openssl-ci-bot commit SHA. Leave empty for a normal manual run.'
+        required: false
+        type: string
+      check_run_id:
+        description: 'Internal: openssl-ci-bot check-run ID. Leave empty for a normal manual run.'
+        required: false
+        type: string
+
+# Keep in sync with openssl-ci-bot's run-name parser, and with the Actions statistics
+# collector that attributes CI load by parsing this same string. Both break silently.
+run-name: >-
+  ${{ github.event.inputs.pr && format('ci-dispatch pr={0} head={1} check_run_id={2}', github.event.inputs.pr, github.event.inputs.head_sha, github.event.inputs.check_run_id) || github.workflow }}
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.inputs.pr || github.run_id }}
+  cancel-in-progress: true

 permissions:
   contents: read

 jobs:
+  # Only a dispatch carries inputs, so this is skipped on the nightly cron.
+  validate-dispatch-inputs:
+    if: |
+      github.repository == 'openssl/openssl' &&
+      (inputs.pr != '' || inputs.head_sha != '' || inputs.check_run_id != '')
+    uses: ./.github/workflows/validate-dispatch-inputs.yml
+    with:
+      pr: ${{ inputs.pr }}
+      head_sha: ${{ inputs.head_sha }}
+      check_run_id: ${{ inputs.check_run_id }}
+
   alpine:
-    if: github.repository == 'openssl/openssl'
+    needs: [validate-dispatch-inputs]
+    if: |
+      github.repository == 'openssl/openssl' &&
+      !cancelled() &&
+      (needs.validate-dispatch-inputs.result == 'success' || needs.validate-dispatch-inputs.result == 'skipped')
     strategy:
       fail-fast: false
       matrix:
@@ -39,6 +76,7 @@ jobs:
     - uses: actions/checkout@v6
       with:
         persist-credentials: false
+        ref: ${{ github.event.inputs.head_sha || github.sha }}
     - name: config
       run: |
         ./config --strict-warnings --banner=Configured no-shared enable-fips \
@@ -55,7 +93,11 @@ jobs:
       run: make test HARNESS_JOBS=${HARNESS_JOBS:-4} LHASH_WORKERS=${LHASH_WORKERS:-16}

   linux:
-    if: github.repository == 'openssl/openssl'
+    needs: [validate-dispatch-inputs]
+    if: |
+      github.repository == 'openssl/openssl' &&
+      !cancelled() &&
+      (needs.validate-dispatch-inputs.result == 'success' || needs.validate-dispatch-inputs.result == 'skipped')
     strategy:
       fail-fast: false
       matrix:
@@ -91,6 +133,7 @@ jobs:
     - uses: actions/checkout@v6
       with:
         persist-credentials: false
+        ref: ${{ github.event.inputs.head_sha || github.sha }}
     - name: install packages
       run: ${{ matrix.zoo.install }}
     - name: config
@@ -107,7 +150,11 @@ jobs:
       run: make test HARNESS_JOBS=${HARNESS_JOBS:-4} LHASH_WORKERS=${LHASH_WORKERS:-16}

   macos:
-    if: github.repository == 'openssl/openssl'
+    needs: [validate-dispatch-inputs]
+    if: |
+      github.repository == 'openssl/openssl' &&
+      !cancelled() &&
+      (needs.validate-dispatch-inputs.result == 'success' || needs.validate-dispatch-inputs.result == 'skipped')
     strategy:
       fail-fast: false
       matrix:
@@ -117,6 +164,7 @@ jobs:
     - uses: actions/checkout@v6
       with:
         persist-credentials: false
+        ref: ${{ github.event.inputs.head_sha || github.sha }}
     - name: checkout fuzz/corpora submodule
       run: git submodule update --init --depth 1 fuzz/corpora
     - name: config
@@ -133,7 +181,11 @@ jobs:
       run: make test HARNESS_JOBS=${HARNESS_JOBS:-4} LHASH_WORKERS=${LHASH_WORKERS:-16}

   windows:
-    if: github.repository == 'openssl/openssl'
+    needs: [validate-dispatch-inputs]
+    if: |
+      github.repository == 'openssl/openssl' &&
+      !cancelled() &&
+      (needs.validate-dispatch-inputs.result == 'success' || needs.validate-dispatch-inputs.result == 'skipped')
     strategy:
       fail-fast: false
       matrix:
@@ -147,6 +199,7 @@ jobs:
     - uses: actions/checkout@v6
       with:
         persist-credentials: false
+        ref: ${{ github.event.inputs.head_sha || github.sha }}
     - name: checkout fuzz/corpora submodule
       run: git submodule update --init --depth 1 fuzz/corpora
     - name: install nasm
@@ -198,12 +251,20 @@ jobs:
         call "${{ matrix.platform.vcvars }}"
         jom test VERBOSE_FAILURE=yes HARNESS_JOBS=4 LHASH_WORKERS=16

+  # GitHub-hosted, so pull-request code may build here: coverage bought, not risked. The
+  # repository clause is new -- a cron in a fork expresses nobody's intent.
   linux-arm64:
+    needs: [validate-dispatch-inputs]
+    if: |
+      github.repository == 'openssl/openssl' &&
+      !cancelled() &&
+      (needs.validate-dispatch-inputs.result == 'success' || needs.validate-dispatch-inputs.result == 'skipped')
     runs-on: ubuntu-24.04-arm
     steps:
     - uses: actions/checkout@v6
       with:
         persist-credentials: false
+        ref: ${{ github.event.inputs.head_sha || github.sha }}
     - name: config
       run: ./config --strict-warnings enable-fips enable-ec_nistp_64_gcc_128 enable-md2 enable-rc5 enable-trace
     - name: config dump
@@ -216,11 +277,17 @@ jobs:
       run: make test HARNESS_JOBS=${HARNESS_JOBS:-4} LHASH_WORKERS=${LHASH_WORKERS:-16}

   linux-x86:
+    needs: [validate-dispatch-inputs]
+    if: |
+      github.repository == 'openssl/openssl' &&
+      !cancelled() &&
+      (needs.validate-dispatch-inputs.result == 'success' || needs.validate-dispatch-inputs.result == 'skipped')
     runs-on: ubuntu-latest
     steps:
     - uses: actions/checkout@v6
       with:
         persist-credentials: false
+        ref: ${{ github.event.inputs.head_sha || github.sha }}
     - name: run container
       run: |
         CONTAINER_ID=$(podman run -d -v $(pwd):/mnt -w /mnt --platform=linux/i386 docker.io/i386/debian:13 sleep infinity)
@@ -258,11 +325,18 @@ jobs:

   linux-ppc64le:
     runs-on: linux-ppc64le
-    if: github.repository == 'openssl/openssl'
+    needs: [validate-dispatch-inputs]
+    if: |
+      github.repository == 'openssl/openssl' &&
+      ( github.event_name == 'schedule' ||
+        (github.event_name == 'workflow_dispatch' && inputs.pr == '') ) &&
+      !cancelled() &&
+      (needs.validate-dispatch-inputs.result == 'success' || needs.validate-dispatch-inputs.result == 'skipped')
     steps:
     - uses: actions/checkout@v6
       with:
         persist-credentials: false
+        ref: ${{ github.event.inputs.head_sha || github.sha }}
     - name: config
       run: ./config --strict-warnings enable-fips enable-ec_nistp_64_gcc_128 enable-md2 enable-rc5 enable-trace
     - name: config dump
@@ -278,11 +352,18 @@ jobs:

   linux-s390x:
     runs-on: linux-s390x
-    if: github.repository == 'openssl/openssl'
+    needs: [validate-dispatch-inputs]
+    if: |
+      github.repository == 'openssl/openssl' &&
+      ( github.event_name == 'schedule' ||
+        (github.event_name == 'workflow_dispatch' && inputs.pr == '') ) &&
+      !cancelled() &&
+      (needs.validate-dispatch-inputs.result == 'success' || needs.validate-dispatch-inputs.result == 'skipped')
     steps:
     - uses: actions/checkout@v6
       with:
         persist-credentials: false
+        ref: ${{ github.event.inputs.head_sha || github.sha }}
     - name: config
       run: ./config --strict-warnings enable-fips enable-md2 enable-rc5 enable-trace
     - name: config dump
@@ -298,11 +379,18 @@ jobs:

   linux-riscv64:
     runs-on: linux-riscv64
-    if: github.repository == 'openssl/openssl'
+    needs: [validate-dispatch-inputs]
+    if: |
+      github.repository == 'openssl/openssl' &&
+      ( github.event_name == 'schedule' ||
+        (github.event_name == 'workflow_dispatch' && inputs.pr == '') ) &&
+      !cancelled() &&
+      (needs.validate-dispatch-inputs.result == 'success' || needs.validate-dispatch-inputs.result == 'skipped')
     steps:
     - uses: actions/checkout@v6
       with:
         persist-credentials: false
+        ref: ${{ github.event.inputs.head_sha || github.sha }}
     - name: config
       run: ./config enable-fips enable-ec_nistp_64_gcc_128 enable-md2 enable-rc5 enable-trace
     - name: config dump
@@ -318,11 +406,16 @@ jobs:

   freebsd-x86_64:
     runs-on: ubuntu-latest
-    if: github.repository == 'openssl/openssl'
+    needs: [validate-dispatch-inputs]
+    if: |
+      github.repository == 'openssl/openssl' &&
+      !cancelled() &&
+      (needs.validate-dispatch-inputs.result == 'success' || needs.validate-dispatch-inputs.result == 'skipped')
     steps:
     - uses: actions/checkout@v6
       with:
         persist-credentials: false
+        ref: ${{ github.event.inputs.head_sha || github.sha }}
     - name: config
       uses: cross-platform-actions/action@46e8d7fb25520a8d6c64fd2b7a1192611da98eda #v0.30.0
       with:
@@ -357,9 +450,16 @@ jobs:

   openbsd-x86_64:
     runs-on: ubuntu-latest
-    if: github.repository == 'openssl/openssl'
+    needs: [validate-dispatch-inputs]
+    if: |
+      github.repository == 'openssl/openssl' &&
+      !cancelled() &&
+      (needs.validate-dispatch-inputs.result == 'success' || needs.validate-dispatch-inputs.result == 'skipped')
     steps:
     - uses: actions/checkout@v6
+      with:
+        persist-credentials: false
+        ref: ${{ github.event.inputs.head_sha || github.sha }}
     - name: config
       uses: cross-platform-actions/action@46e8d7fb25520a8d6c64fd2b7a1192611da98eda #v0.30.0
       with:
diff --git a/.github/workflows/oss-fuzz.yml b/.github/workflows/oss-fuzz.yml
index 33af299a84..0cdf1864bb 100644
--- a/.github/workflows/oss-fuzz.yml
+++ b/.github/workflows/oss-fuzz.yml
@@ -10,12 +10,48 @@ on:
   schedule:
     - cron: '50 01 * * *'
   workflow_dispatch:
+    inputs:
+      pr:
+        description: 'Internal: openssl-ci-bot PR number. Leave empty for a normal manual run.'
+        required: false
+        type: string
+      head_sha:
+        description: 'Internal: openssl-ci-bot commit SHA. Leave empty for a normal manual run.'
+        required: false
+        type: string
+      check_run_id:
+        description: 'Internal: openssl-ci-bot check-run ID. Leave empty for a normal manual run.'
+        required: false
+        type: string
+
+# Keep in sync with openssl-ci-bot's run-name parser, and with the Actions statistics
+# collector that attributes CI load by parsing this same string. Both break silently.
+run-name: >-
+  ${{ github.event.inputs.pr && format('ci-dispatch pr={0} head={1} check_run_id={2}', github.event.inputs.pr, github.event.inputs.head_sha, github.event.inputs.check_run_id) || github.workflow }}
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.inputs.pr || github.run_id }}
+  cancel-in-progress: true
+
 permissions:
   contents: read

 jobs:
+  # Only a dispatch carries inputs, so this is skipped on the nightly cron.
+  validate-dispatch-inputs:
+    if: inputs.pr != '' || inputs.head_sha != '' || inputs.check_run_id != ''
+    uses: ./.github/workflows/validate-dispatch-inputs.yml
+    with:
+      pr: ${{ inputs.pr }}
+      head_sha: ${{ inputs.head_sha }}
+      check_run_id: ${{ inputs.check_run_id }}
+
   Fuzzing:
-    if: github.event_name != 'schedule' || github.repository == 'openssl/openssl'
+    needs: [validate-dispatch-inputs]
+    if: >-
+      (github.event_name != 'schedule' || github.repository == 'openssl/openssl') &&
+      !cancelled() &&
+      (needs.validate-dispatch-inputs.result == 'success' || needs.validate-dispatch-inputs.result == 'skipped')
     runs-on: ubuntu-latest
     steps:
     - name: Clear unnecessary files
@@ -23,15 +59,21 @@ jobs:
         df
         sudo rm -rf /usr/share/dotnet /usr/share/swift /usr/local/.ghcup /usr/local/share/powershell /usr/local/share/chromium /usr/local/lib/android /usr/local/lib/node_modules
         df
+    - uses: actions/checkout@v6
+      with:
+        persist-credentials: false
+        ref: ${{ github.event.inputs.head_sha || github.sha }}
     - name: Build Fuzzers
       uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
       with:
         oss-fuzz-project-name: 'openssl'
+        project-src-path: ${{ github.workspace }}
         dry-run: false
     - name: Run Fuzzers
       uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
       with:
         oss-fuzz-project-name: 'openssl'
+        project-src-path: ${{ github.workspace }}
         fuzz-seconds: 600
         dry-run: false
     - name: Upload Crash
diff --git a/.github/workflows/riscv-more-cross-compiles.yml b/.github/workflows/riscv-more-cross-compiles.yml
index 069495e0f6..61ea54eb4b 100644
--- a/.github/workflows/riscv-more-cross-compiles.yml
+++ b/.github/workflows/riscv-more-cross-compiles.yml
@@ -22,11 +22,42 @@ on:
   schedule:
     - cron: '35 02 * * *'
   workflow_dispatch:
+    inputs:
+      pr:
+        description: 'Internal: openssl-ci-bot PR number. Leave empty for a normal manual run.'
+        required: false
+        type: string
+      head_sha:
+        description: 'Internal: openssl-ci-bot commit SHA. Leave empty for a normal manual run.'
+        required: false
+        type: string
+      check_run_id:
+        description: 'Internal: openssl-ci-bot check-run ID. Leave empty for a normal manual run.'
+        required: false
+        type: string
+
+# Keep in sync with openssl-ci-bot's run-name parser, and with the Actions statistics
+# collector that attributes CI load by parsing this same string. Both break silently.
+run-name: >-
+  ${{ github.event.inputs.pr && format('ci-dispatch pr={0} head={1} check_run_id={2}', github.event.inputs.pr, github.event.inputs.head_sha, github.event.inputs.check_run_id) || github.workflow }}
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.inputs.pr || github.run_id }}
+  cancel-in-progress: true

 permissions:
   contents: read

 jobs:
+  # Only a dispatch carries inputs, so this is skipped on every other trigger.
+  validate-dispatch-inputs:
+    if: inputs.pr != '' || inputs.head_sha != '' || inputs.check_run_id != ''
+    uses: ./.github/workflows/validate-dispatch-inputs.yml
+    with:
+      pr: ${{ inputs.pr }}
+      head_sha: ${{ inputs.head_sha }}
+      check_run_id: ${{ inputs.check_run_id }}
+
   cross-compilation-riscv:
     # pull request title contains 'riscv'
     # pull request title contains 'RISC-V'
@@ -34,7 +65,11 @@ jobs:
     # push event commit message contains '[riscv ci]'
     # cron job
     # manual dispatch
-    if: contains(github.event.pull_request.title, 'riscv') || contains(github.event.pull_request.title, 'RISC-V') || contains(github.event.pull_request.body, '[riscv ci]') || contains(github.event.head_commit.message, '[riscv ci]') || (github.event_name == 'schedule' && github.repository == 'openssl/openssl') || github.event_name == 'workflow_dispatch'
+    needs: [validate-dispatch-inputs]
+    if: >-
+      (contains(github.event.pull_request.title, 'riscv') || contains(github.event.pull_request.title, 'RISC-V') || contains(github.event.pull_request.body, '[riscv ci]') || contains(github.event.head_commit.message, '[riscv ci]') || (github.event_name == 'schedule' && github.repository == 'openssl/openssl') || github.event_name == 'workflow_dispatch') &&
+      !cancelled() &&
+      (needs.validate-dispatch-inputs.result == 'success' || needs.validate-dispatch-inputs.result == 'skipped')
     strategy:
       fail-fast: false
       matrix:
@@ -259,6 +294,7 @@ jobs:
     - uses: actions/checkout@v6
       with:
         persist-credentials: false
+        ref: ${{ github.event.inputs.head_sha || github.sha }}
     - name: checkout fuzz/corpora submodule
       run: git submodule update --init --depth 1 fuzz/corpora

@@ -307,14 +343,17 @@ jobs:
           ./util/opensslwrap.sh info -cpusettings | \
           grep -qE "${{ matrix.platform.capscheck }}"

+    # A dispatched run takes the push tier, not the pull_request one below: it exists to run
+    # pre-merge what currently only runs post-merge, and evp-only is the weaker signal. Legs
+    # setting `tests: none` stay build-only on every trigger, by design.
     - name: make all tests
-      if: github.event_name == 'push' && matrix.platform.tests == ''
+      if: (github.event_name == 'push' || inputs.pr != '') && matrix.platform.tests == ''
       run: |
         .github/workflows/make-test \
                   TESTS="-test_afalg" \
                   QEMU_LD_PREFIX=/usr/${{ matrix.platform.arch }}
     - name: make some tests
-      if: github.event_name == 'push' && matrix.platform.tests != 'none' && matrix.platform.tests != ''
+      if: (github.event_name == 'push' || inputs.pr != '') && matrix.platform.tests != 'none' && matrix.platform.tests != ''
       run: |
         .github/workflows/make-test \
                   TESTS="${{ matrix.platform.tests }} -test_afalg" \