ci: stop concurrent CodeQL and duplicate gate runs on PRs and develop/stage (#10259)

* ci: stop concurrent CodeQL and duplicate gate runs on PRs and develop/stage

CodeQL ran through GitHub's code-scanning DEFAULT SETUP, which generates its
workflow server-side (run path `dynamic/github-code-scanning/codeql`). There is
no file to put a `concurrency:` block in, and GitHub does not supersede its own
default-setup runs, so every push to a pull request started another full
analysis alongside the ones already in flight. Measured on #10254 on
2026-09-21: runs 35547460875 and 35548451790 overlapped by 12 minutes, and
35580875040 / 35582701347 / 35582925033 were all running at 09:22. Each run is
two jobs of 25-35 minutes on the 4-vCPU ARC pool.

Converts CodeQL to advanced setup with a real workflow file that reproduces the
default-setup configuration exactly - the two analyses it actually ran
(javascript-typescript, actions; the other three listed languages are aliases),
the default query suite, the default remote threat model, the weekly schedule,
and the ever-k8s-linux-x64-4 pool - and adds the cancel-in-progress policy that
default setup could not have. Branch coverage stays at develop/stage/master
because all three are protected and default setup was scanning all three.

Also closes the remaining concurrency gaps on the PR and develop/stage surface:

- build.yml and secrets-analysis.yml both pair an unfiltered `pull_request:`
  with a `push:` on the same branches, so while a release-cascade PR is open
  (today #10257, stage -> stage-apps) one push fires two runs of the same tree
  in two groups that could never cancel each other. Keying the group on the
  head ref collapses them; qualifying it by head repository keeps a fork branch
  of the same name from claiming the group and cancelling a real build before
  its job skips.
- harvest-secrets-to-openbao.yml was the only workflow of 70 with no
  concurrency block, and two overlapping dispatches can interleave into a torn
  OpenBao snapshot.
- external-uptime-monitor.yml now cancels superseded PR self-tests while still
  never cancelling a live probe, which owns the alert issue.

Deliberately unchanged: the deploy and release workflows keep
`cancel-in-progress: false`, and test-unit.yml keeps it on the stage gate. Each
documents an incident where cancelling caused real harm.

Requires default setup to be disabled first - GitHub rejects CodeQL SARIF
uploads from a workflow while default setup is enabled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ci: collapse duplicate CodeQL runs and teach cspell the OpenBao name

Two review follow-ups on this branch.

Greptile P2: codeql.yml had the same duplicate-run defect this PR fixes in
build.yml and secrets-analysis.yml. Both of its triggers cover
develop/stage/master, so while a cascade pull request is open (develop ->
stage), one push to develop fires a push run on refs/heads/develop and a
pull_request run on refs/pull/N/merge. Keyed on github.ref those are different
strings, so cancel-in-progress could never collapse them and the same tree was
analysed twice. Now keyed on the head ref and qualified by head repository, the
same shape used in the other two files.

Cspell: this PR is the first change to harvest-secrets-to-openbao.yml since the
spellcheck was added, and the action only scans changed files - so `openbao` had
never been checked before and surfaced as an unknown word. Added to .cspell.json
next to the other product names.

Not taken: CodeRabbit asked for the actions in codeql.yml to be pinned to commit
SHAs. Declined as inconsistent rather than wrong - this repository tag-pins 547
`uses:` references against a handful of SHA pins, and there is no dependabot
config to keep SHAs current, so pinning one new file would leave an outlier that
nothing updates. Worth doing repo-wide as a deliberate policy, not here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ci: use US spelling in the codeql concurrency comment

Cspell flagged `analysed` at codeql.yml:60. This repository is US English
throughout (the job is named `Analyze`), so the comment is corrected rather
than the word added to the dictionary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Ruslan Konviser
2026-09-22 11:45:19 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 5fafd072af
commit 58bf570f64
6 changed files with 175 additions and 3 deletions
+20 -1
View File
@@ -42,7 +42,26 @@ on:
# group serializes unrelated builds and silently cancels them (see the image-build starvation
# incident). Within this workflow+ref, only the newest commit is built.
concurrency:
group: build-${{ github.workflow }}-${{ github.ref }}
# Keyed on the HEAD REF, not on `github.ref`, so the two events that fire for one commit land in
# the same group. `pull_request:` above is unfiltered, so while a release-cascade PR is open whose
# head is a build branch (today: #10257, `stage` -> `stage-apps`), a single push to `stage` starts
# BOTH a push run on `refs/heads/stage` AND a pull_request run on `refs/pull/10257/merge`. Under
# `github.ref` those are different strings, so neither cancelled the other and the fleet compiled
# the same tree twice — two 3-hour builds per push, for as long as the cascade PR stays open.
#
# The trade-off, stated plainly: the pull_request run builds the MERGE commit and the push run
# builds the branch head. For a fast-forward cascade those are the same tree and the second build
# is pure waste; if the base has diverged they differ, and collapsing them means only the newer
# event's view is compiled. That is acceptable here because no branch declares a required status
# check, so nothing is gated on the verdict that loses — but it IS a real narrowing, not a free win.
#
# Qualified by the head REPOSITORY as well as the ref. Fork pull requests are skipped at the job
# level, but a skipped job still creates a run that claims the group first — so without the repo
# in the key, a fork branch named `develop` could cancel a genuine `develop` build and then skip.
group: >-
build-${{ github.workflow }}-${{ github.event_name == 'pull_request'
&& format('{0}@{1}', github.event.pull_request.head.repo.full_name, github.event.pull_request.head.ref)
|| format('{0}@{1}', github.repository, github.ref_name) }}
# Was `github.event_name == 'pull_request'`, which left branch pushes uncancelled:
# four merges to develop in an hour meant four full 3-h compiles of commits that were
# already superseded. This job only compiles — nothing outside the run depends on a
+120
View File
@@ -0,0 +1,120 @@
name: 'CodeQL'
# CodeQL moved from GitHub's DEFAULT SETUP to advanced setup for exactly one reason:
# default setup cannot be given a concurrency policy.
#
# Default setup has no workflow file — GitHub generates the run server-side, which is why its
# runs show up under the path `dynamic/github-code-scanning/codeql` instead of this directory.
# There is nowhere to put a `concurrency:` block, and GitHub does not supersede its own runs, so
# every push to a pull request started ANOTHER full analysis alongside the ones already running.
# Measured on #10254 on 2026-09-21: runs 35547460875 (00:21→00:52) and 35548451790 (00:40→01:14)
# overlapped by 12 minutes, and 35580875040 / 35582701347 / 35582925033 were all in flight at
# 09:22. Each run is two jobs of roughly 25-35 minutes on the 4-vCPU ARC pool, so every overlap
# pinned two extra self-hosted runner pods to a commit that had already been superseded.
#
# This file reproduces what default setup was configured to do — same languages, the default
# query suite, the default (remote) threat model, the same weekly schedule and the same runner
# label — and adds the one thing it could not have.
#
# Parity note: default setup listed four languages (actions, javascript, javascript-typescript,
# typescript), but `javascript`, `typescript` and `javascript-typescript` are three names for one
# analysis — its runs only ever produced two jobs, and the uploaded analyses only ever carried the
# categories `/language:javascript-typescript` and `/language:actions`. The matrix below is those
# two, which is the same coverage and not a reduction.
# Branch list is parity, not a widening. Default setup scans the default branch AND every
# protected branch, plus pull requests targeting them — and `develop`, `stage` and `master` are
# all protected on this repository, so default setup was already scanning all three. Confirmed
# against the uploaded analyses, which include `refs/heads/stage` as well as `refs/heads/develop`.
# Listing only `develop` here would have quietly dropped CodeQL from the release cascade.
on:
push:
branches:
- develop
- stage
- master
pull_request:
branches:
- develop
- stage
- master
# Default setup ran a weekly scan so that a new CodeQL release re-examines unchanged code.
# Keeping it means an advisory published after a file was last touched still finds it.
schedule:
- cron: '27 4 * * 1'
workflow_dispatch:
concurrency:
# The point of this file. A newer commit on the same ref wins and the superseded analysis is
# killed. Safe here in a way it is not for the deploy and release workflows: this job only reads
# the checkout and uploads a SARIF result, so a half-finished run leaves nothing behind — the
# next run re-uploads the complete picture for that ref. Nothing downstream is wired to this
# workflow's conclusion (no `workflow_run` consumer), and none of develop/stage/master declares
# a required status check, so a cancelled run cannot wedge a pull request's merge gate either.
# If required checks are ever introduced, re-check this line before trusting it.
#
# Keyed on the HEAD REF rather than `github.ref`, for the same reason and with the same trade-off
# as `build.yml`: both triggers above cover develop/stage/master, so while a cascade pull request
# is open (develop -> stage), one push to `develop` fires a push run on `refs/heads/develop` AND a
# pull_request run on `refs/pull/N/merge`. Under `github.ref` those are different strings, so
# neither could cancel the other and the same tree was analyzed twice. Qualified by the head
# REPOSITORY as well, because a fork pull request's run claims the group before its job skips.
group: >-
${{ github.workflow }}-${{ github.event_name == 'pull_request'
&& format('{0}@{1}', github.event.pull_request.head.repo.full_name, github.event.pull_request.head.ref)
|| format('{0}@{1}', github.repository, github.ref_name) }}
cancel-in-progress: true
# Least privilege at the workflow level; the analyze job widens only what CodeQL needs.
permissions:
contents: read
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Never run jobs for a pull request from a fork (owner decision 2026-09-16); branch PRs and pushes still run.
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
# Same pool default setup used (`runner_label: ever-k8s-linux-x64-4`), read through the repo
# variable the rest of this directory uses so a fleet rename is a one-place change. The
# fallback keeps the workflow runnable if the ARC pool is ever unavailable.
runs-on: ${{ vars.RUNNER_LINUX_X64_4 || 'ubuntu-latest' }}
timeout-minutes: 120
permissions:
# Upload the SARIF results to the code scanning API. Required by every advanced setup.
security-events: write
# Lets the `actions` language analysis read workflow definitions and run metadata.
actions: read
# Fetching CodeQL query packs.
packages: read
contents: read
strategy:
# One language failing must not hide the other language's findings.
fail-fast: false
matrix:
include:
- language: javascript-typescript
build-mode: none
- language: actions
build-mode: none
steps:
- uses: actions/checkout@v5
with:
# Analysis only reads the tree; the checkout credential does not need to outlive the step.
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# No `queries:` and no `config:` on purpose. Default setup ran `query_suite: default`
# with `threat_model: remote`, and both of those ARE the action's defaults — naming them
# explicitly would not change the analysis, it would only create a second place to keep
# in sync. Widen to `security-extended` here if the team ever decides to.
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
# Produces the same `/language:<lang>` category default setup uploaded, so the existing
# alerts are matched rather than duplicated as a new tool's findings.
category: '/language:${{ matrix.language }}'
@@ -74,7 +74,13 @@ concurrency:
# can never delay or displace a real production probe.
group: >-
external-uptime-monitor-${{ github.event_name == 'pull_request' && github.ref || 'live' }}
cancel-in-progress: false
# Per-event, because the two halves of that group want opposite answers. A LIVE probe must never
# be cancelled: it owns the alert issue, and killing it mid-flight can leave an incident open with
# no run left to close it. A pull-request self-test owns nothing — alerting is force-disabled on
# `pull_request` (see "Decide alerting mode") — so when a reviewer pushes again, the older
# self-test is pure waste and the newest commit should win, which is what this repository does
# everywhere else on pull requests.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
probe:
@@ -8,6 +8,22 @@
name: harvest-secrets-to-openbao
on:
workflow_dispatch: {}
# The only workflow in this directory that had no concurrency policy at all.
#
# The group is deliberately NOT keyed on `github.ref`. Both destination paths are derived from
# `github.repository` alone (ROLE/DEST below), so a dispatch from any branch writes the SAME two
# OpenBao keys — a ref-scoped group would serialize nothing. The write is two separate, non-atomic
# POSTs whose payloads are frozen at job start, so two overlapping dispatches can interleave into
# a torn snapshot: secrets from one run and vars from the other.
#
# `cancel-in-progress: false`, not true. Killing a harvest mid-write is the one outcome worse than
# making the second operator wait — it would leave the destination half-updated with no signal that
# it happened. Runs are manual and take seconds, so queueing costs nothing.
concurrency:
group: harvest-secrets-to-openbao-${{ github.repository }}
cancel-in-progress: false
permissions:
id-token: write # for GitHub OIDC
contents: read
+10 -1
View File
@@ -7,7 +7,16 @@ on:
pull_request:
concurrency:
group: ${{ github.ref }}-${{ github.workflow }}
# Same shape as `build.yml`, for the same reason and with the same trade-off written out there:
# `pull_request:` here is unfiltered, so while a cascade PR whose head is `develop` is open, one
# push to `develop` fires a push run and a pull_request run on the same tree, in two groups that
# could never cancel each other — two full-history TruffleHog scans instead of one. Keying on the
# head ref collapses them; qualifying by the head repository keeps a fork branch of the same name
# from claiming this repository's group and cancelling a real scan before its job skips.
group: >-
${{ github.workflow }}-${{ github.event_name == 'pull_request'
&& format('{0}@{1}', github.event.pull_request.head.repo.full_name, github.event.pull_request.head.ref)
|| format('{0}@{1}', github.repository, github.ref_name) }}
cancel-in-progress: true
# Least-privilege scope for the automatic GITHUB_TOKEN.