From 1dddd94b387478c76be6b2c4fe9f41d00dbcb241 Mon Sep 17 00:00:00 2001 From: Ivan Tugay Date: Fri, 25 Sep 2026 17:16:47 +0300 Subject: [PATCH 1/3] ci: gated pipeline running ci, CodeQL, Semgrep and Snyk in parallel Adds snyk.yml (free Snyk CLI, skips with a notice without SNYK_TOKEN) and pipeline.yml, which calls ci.yml, codeql.yml, semgrep.yml and snyk.yml as reusable workflows and gates on all four. The child workflows drop their own push/pull_request/schedule triggers. --- .github/workflows/ci.yml | 7 ++- .github/workflows/codeql.yml | 37 ++++++++++++++ .github/workflows/pipeline.yml | 89 ++++++++++++++++++++++++++++++++++ .github/workflows/semgrep.yml | 32 ++++++++++++ .github/workflows/snyk.yml | 67 +++++++++++++++++++++++++ 5 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/pipeline.yml create mode 100644 .github/workflows/semgrep.yml create mode 100644 .github/workflows/snyk.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d7d730..b6fb4fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,10 @@ name: CI on: - pull_request: + # Push to main, non-draft pull requests and the weekly schedule run this through + # pipeline.yml (`workflow_call`), in parallel with the other checks and behind its `gate` job. + workflow_call: + workflow_dispatch: permissions: actions: read @@ -9,7 +12,7 @@ permissions: pull-requests: write concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ci-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..b7f0b26 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,37 @@ +# CodeQL code scanning for every language in the repo that supports `build-mode: none`; +# results land in the Security tab. Repo default setup must stay off — +# GitHub rejects advanced-workflow uploads while it is on. +name: codeql + +on: + # Push to main, non-draft pull requests and the weekly schedule run this through + # pipeline.yml (`workflow_call`), in parallel with the other checks and behind its `gate` job. + workflow_call: + workflow_dispatch: + +jobs: + analyze: + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + runs-on: ubuntu-latest + permissions: + security-events: write + contents: read + actions: read + strategy: + fail-fast: false + matrix: + # `build-mode: none` for all: every language is extracted from source, so no build runs here. + # Not scanned: swift (no `build-mode: none` support), java-kotlin (Kotlin only, and + # none mode extracts Java only), ruby (only a CocoaPods Podfile, no Ruby sources), python + # (only vendored WAMR test scripts). + language: [actions, c-cpp, javascript-typescript, rust] + steps: + - uses: actions/checkout@v7 + - uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: none + queries: security-and-quality + - uses: github/codeql-action/analyze@v4 + with: + category: /language:${{ matrix.language }} diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml new file mode 100644 index 0000000..93a85e5 --- /dev/null +++ b/.github/workflows/pipeline.yml @@ -0,0 +1,89 @@ +# Gated pipeline. ci, codeql, semgrep and snyk run in parallel as reusable workflows; `gate` +# passes only when all four succeeded. Anything downstream (deploy, release, publish) must +# `needs: gate`. Draft pull requests skip every job (and the gate passes) until marked ready. +name: pipeline + +on: + push: + branches: [main] + pull_request: + branches: [main] + types: [opened, synchronize, reopened, ready_for_review] + schedule: + - cron: "17 4 * * 1" + workflow_dispatch: + +concurrency: + group: pipeline-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + +jobs: + ci: + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + uses: ./.github/workflows/ci.yml + secrets: inherit + # ci.yml's jobs request these (e.g. revert-on-failure); a called workflow cannot exceed them. + permissions: + actions: read + contents: read + pull-requests: write + + codeql: + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + uses: ./.github/workflows/codeql.yml + secrets: inherit + permissions: + security-events: write + contents: read + actions: read + + semgrep: + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + uses: ./.github/workflows/semgrep.yml + secrets: inherit + permissions: + security-events: write + contents: read + actions: read + + snyk: + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + uses: ./.github/workflows/snyk.yml + secrets: inherit + permissions: + security-events: write + contents: read + actions: read + + gate: + needs: [ci, codeql, semgrep, snyk] + if: always() + runs-on: ubuntu-latest + steps: + - name: Require every job to succeed + env: + DRAFT_PR: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft }} + R_CI: ${{ needs.ci.result }} + R_CODEQL: ${{ needs.codeql.result }} + R_SEMGREP: ${{ needs.semgrep.result }} + R_SNYK: ${{ needs.snyk.result }} + run: | + if [ "$DRAFT_PR" = "true" ]; then + echo "::notice title=gate::Draft pull request: pipeline jobs skipped until ready for review." + exit 0 + fi + # Skipped counts as a failure here: outside a draft PR every job must actually run. + fail=0 + for pair in "ci=$R_CI" "codeql=$R_CODEQL" "semgrep=$R_SEMGREP" "snyk=$R_SNYK"; do + name=${pair%%=*} + result=${pair#*=} + echo "$name: $result" + if [ "$result" != "success" ]; then + echo "::error title=gate::$name finished with '$result'" + fail=1 + fi + done + exit "$fail" diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 0000000..55e4ccb --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,32 @@ +# Semgrep static analysis with the free open-source CLI and the community `p/default` registry +# ruleset (no Semgrep account or token). Results land in the Security tab (Code scanning, +# category `semgrep`). Findings never fail the job, so this workflow does not block CI. +name: semgrep + +on: + # Push to main, non-draft pull requests and the weekly schedule run this through + # pipeline.yml (`workflow_call`), in parallel with the other checks and behind its `gate` job. + workflow_call: + workflow_dispatch: + +jobs: + scan: + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + runs-on: ubuntu-latest + permissions: + security-events: write + contents: read + actions: read + container: + # semgrep/semgrep:1.177.0 + image: semgrep/semgrep@sha256:acaac22ffc7b7cc5926de0751b223bce0b2491c33d18422fa72f632c78d81198 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + # No `--error`: findings are reported, not enforced. Metrics off since no account is used. + - name: Semgrep scan + run: semgrep scan --config p/default --sarif --output semgrep.sarif --metrics off + - uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 + if: always() + with: + sarif_file: semgrep.sarif + category: semgrep diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml new file mode 100644 index 0000000..ddf16f3 --- /dev/null +++ b/.github/workflows/snyk.yml @@ -0,0 +1,67 @@ +# Snyk Open Source dependency scan with the free Snyk CLI. Results land in the Security tab +# (Code scanning, category `snyk`); findings never fail the job. Runs from pipeline.yml. +# +# Needs the repo secret SNYK_TOKEN. Without it every step is skipped with a notice and the job +# succeeds, so the pipeline gate stays green until the token is added. +# +# Ecosystems in this repo: Cargo (Rust), pnpm/npm workspaces, Gradle Kotlin DSL (Android), Swift Package Manager (Package.resolved), CocoaPods (Podfile), pip (requirements.txt), Dockerfiles. +# pnpm/npm, SwiftPM and Gradle are attempted; pip needs its packages installed and CocoaPods needs Podfile.lock, so those may show as unresolved. Dockerfiles are not scanned (`snyk container test` needs built images). +# Rust/Cargo: Snyk documents that `snyk test` / `snyk monitor` do not support Cargo projects +# (Rust is limited to `snyk sbom test` on a CycloneDX/SPDX SBOM, and API use needs a paid plan), +# so `--all-projects` does not cover Cargo.lock here. +name: snyk + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + runs-on: ubuntu-latest + permissions: + security-events: write + contents: read + actions: read + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + steps: + - name: Check for SNYK_TOKEN + id: token + run: | + if [ -z "$SNYK_TOKEN" ]; then + echo "::notice title=snyk skipped::SNYK_TOKEN secret is not set; Snyk scan skipped." + echo "present=false" >> "$GITHUB_OUTPUT" + else + echo "present=true" >> "$GITHUB_OUTPUT" + fi + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + if: steps.token.outputs.present == 'true' + - uses: snyk/actions/setup@9adf32b1121593767fc3c057af55b55db032dc04 # v1.0.0 + if: steps.token.outputs.present == 'true' + # Exit 1 = vulnerabilities found (reported, not enforced); 2/3 = scan error or no + # supported manifest (warned, not enforced). + - name: Snyk test + if: steps.token.outputs.present == 'true' + run: | + code=0 + snyk test --all-projects --sarif-file-output=snyk.sarif || code=$? + case "$code" in + 0) ;; + 1) echo "::notice title=snyk::Vulnerable dependencies found; see Security > Code scanning (category snyk)." ;; + *) echo "::warning title=snyk::snyk test exited $code (scan error or no supported project); not failing the job." ;; + esac + - name: Snyk monitor + if: steps.token.outputs.present == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + code=0 + snyk monitor --all-projects || code=$? + if [ "$code" != 0 ]; then echo "::warning title=snyk::snyk monitor exited $code"; fi + - uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 + if: steps.token.outputs.present == 'true' && hashFiles('snyk.sarif') != '' + with: + sarif_file: snyk.sarif + category: snyk From 6f4236db3efba508d49cd1e46cbd9d21d56e5193 Mon Sep 17 00:00:00 2001 From: Ivan Tugay Date: Fri, 25 Sep 2026 17:55:33 +0300 Subject: [PATCH 2/3] ci: gate only on real results; pass secrets explicitly; pin CodeQL actions Skip the gate on draft PRs instead of passing it, so a draft run cannot leave a green gate check behind; replace secrets: inherit with the secrets each child uses (Semgrep secrets-inherit); SHA-pin the checkout/CodeQL actions (Semgrep mutable-action-tag). --- .github/workflows/ci.yml | 3 +++ .github/workflows/codeql.yml | 6 +++--- .github/workflows/pipeline.yml | 22 ++++++++++------------ .github/workflows/snyk.yml | 3 +++ 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6fb4fb..2a22135 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,9 @@ on: # Push to main, non-draft pull requests and the weekly schedule run this through # pipeline.yml (`workflow_call`), in parallel with the other checks and behind its `gate` job. workflow_call: + secrets: + CODECOV_TOKEN: + required: false workflow_dispatch: permissions: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b7f0b26..6d5de97 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -26,12 +26,12 @@ jobs: # (only vendored WAMR test scripts). language: [actions, c-cpp, javascript-typescript, rust] steps: - - uses: actions/checkout@v7 - - uses: github/codeql-action/init@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 with: languages: ${{ matrix.language }} build-mode: none queries: security-and-quality - - uses: github/codeql-action/analyze@v4 + - uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 with: category: /language:${{ matrix.language }} diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 93a85e5..023d69a 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -1,6 +1,7 @@ # Gated pipeline. ci, codeql, semgrep and snyk run in parallel as reusable workflows; `gate` # passes only when all four succeeded. Anything downstream (deploy, release, publish) must -# `needs: gate`. Draft pull requests skip every job (and the gate passes) until marked ready. +# `needs: gate`. Draft pull requests skip every job, the gate included, until marked ready. +# Secrets are passed explicitly (no `secrets: inherit`): each child gets only what it uses. name: pipeline on: @@ -24,7 +25,8 @@ jobs: ci: if: github.event_name != 'pull_request' || !github.event.pull_request.draft uses: ./.github/workflows/ci.yml - secrets: inherit + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} # ci.yml's jobs request these (e.g. revert-on-failure); a called workflow cannot exceed them. permissions: actions: read @@ -34,7 +36,6 @@ jobs: codeql: if: github.event_name != 'pull_request' || !github.event.pull_request.draft uses: ./.github/workflows/codeql.yml - secrets: inherit permissions: security-events: write contents: read @@ -43,7 +44,6 @@ jobs: semgrep: if: github.event_name != 'pull_request' || !github.event.pull_request.draft uses: ./.github/workflows/semgrep.yml - secrets: inherit permissions: security-events: write contents: read @@ -52,7 +52,8 @@ jobs: snyk: if: github.event_name != 'pull_request' || !github.event.pull_request.draft uses: ./.github/workflows/snyk.yml - secrets: inherit + secrets: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} permissions: security-events: write contents: read @@ -60,22 +61,19 @@ jobs: gate: needs: [ci, codeql, semgrep, snyk] - if: always() + # always(): run even when a need failed or was cancelled, so the gate reports it. Skipped on + # draft PRs (like every other job) so a draft run never leaves a green `gate` check behind. + if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) runs-on: ubuntu-latest steps: - name: Require every job to succeed env: - DRAFT_PR: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft }} R_CI: ${{ needs.ci.result }} R_CODEQL: ${{ needs.codeql.result }} R_SEMGREP: ${{ needs.semgrep.result }} R_SNYK: ${{ needs.snyk.result }} run: | - if [ "$DRAFT_PR" = "true" ]; then - echo "::notice title=gate::Draft pull request: pipeline jobs skipped until ready for review." - exit 0 - fi - # Skipped counts as a failure here: outside a draft PR every job must actually run. + # Only `success` passes: failure, cancelled and skipped all fail the gate. fail=0 for pair in "ci=$R_CI" "codeql=$R_CODEQL" "semgrep=$R_SEMGREP" "snyk=$R_SNYK"; do name=${pair%%=*} diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml index ddf16f3..d44be71 100644 --- a/.github/workflows/snyk.yml +++ b/.github/workflows/snyk.yml @@ -13,6 +13,9 @@ name: snyk on: workflow_call: + secrets: + SNYK_TOKEN: + required: false workflow_dispatch: permissions: From fc5eeaf1aa5b62862bf073cfff977311b4a2ba63 Mon Sep 17 00:00:00 2001 From: Ivan Tugay Date: Fri, 25 Sep 2026 18:25:42 +0300 Subject: [PATCH 3/3] ci: gate uses !cancelled() instead of always() always() made a cancelled run wait for a runner to execute the gate, holding the pipeline concurrency group so the superseding run stayed pending. --- .github/workflows/pipeline.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 023d69a..63cad6c 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -61,9 +61,11 @@ jobs: gate: needs: [ci, codeql, semgrep, snyk] - # always(): run even when a need failed or was cancelled, so the gate reports it. Skipped on - # draft PRs (like every other job) so a draft run never leaves a green `gate` check behind. - if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) + # !cancelled(): run even when a need failed, was cancelled or skipped, so the gate reports it, + # but not when the whole run is cancelled (always() would hold the run and its concurrency + # group until a runner picks up the gate). Skipped on draft PRs like every other job, so a + # draft run never leaves a green `gate` check behind. + if: ${{ !cancelled() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }} runs-on: ubuntu-latest steps: - name: Require every job to succeed