A CodeQL upgrade can change more than the command-line executable. Custom queries compile against language libraries, and an import that worked with the previous library pack can stop the analysis before a database is queried. That is a different failure from a newly reported alert or a changed result set: the workflow may never reach the point where it produces results at all.
CodeQL 2.26.3 removes codeql.actions.security.SelfHostedQuery from the GitHub Actions library. GitHub’s changelog explains why: runner labels do not reliably distinguish self-hosted runners from managed runners. If a repository keeps custom Actions queries, check for that import before changing the CodeQL Action, CLI, or query-pack version.
This guide adds a small, read-only source gate. It searches only the custom-query directories that the repository owns, exits nonzero when the removed module is found, and prints the file and line for review. The method was tested in a disposable Linux AMD64 Git repository. A synthetic query containing the old import made git grep exit 0 and print the import line; a query without it made the same scoped search exit 1 with no output. The CodeQL 2.26.3 Linux archive was also checksum-verified before inspection, and its compatible github/codeql source revision contained no definition of SelfHostedQuery.
This is a migration check, not a replacement security query. Do not delete an import and assume the old policy is still represented. Read the custom query, decide what it was attempting to establish, and use workflow permissions, runner-group controls, environment protection, and the supported CodeQL model to express a reviewable security boundary.
Start with the dependency change, not a workflow failure
The breaking change is specific: a module was removed from the GitHub Actions library in CodeQL 2.26.3. It is not a statement that every workflow using a self-hosted runner is unsafe, nor that every custom Actions query needs an update. A repository is affected only if a custom query imports the removed module directly or imports another local library that eventually depends on it.
That distinction makes a text search a useful first gate. It is fast, has no access to a repository’s Actions secrets, and does not need a CodeQL database. It answers the narrow question that should be answered before an upgrade: does the source tree still name the removed API?
First identify the local directories that hold custom CodeQL queries. Common locations include .github/codeql, custom-codeql, and a dedicated query-pack directory. Do not scan the entire home directory or a downloaded CodeQL distribution. Generated files, package caches, vendored query packs, and another repository can produce a match that does not belong to the workflow being changed.
From the root of the repository being reviewed, run a scoped search. Adjust the paths to match the repository, and omit a path that does not exist rather than adding a broad . argument.
git grep -n -E 'SelfHostedQuery' -- \
.github/codeql custom-codeql
git grep searches tracked source files, which is useful in a change gate: a local editor backup or untracked experiment does not become an upgrade blocker. -n supplies the line number, and -- separates the pattern from repository paths. Keep the module name in the check rather than matching the phrase “self-hosted”. Comments, documentation, and runner labels can legitimately use that phrase without importing the removed QL module.
An exit status of 1 means the scoped search found no match. An exit status of 0 means it found at least one match and the upgrade should stop for a query review. Any other status is an execution problem, such as a misspelled path or a Git failure, and should fail the gate rather than being treated as a clean result.
Make the negative result explicit in CI
A plain search command has an important shell property: no match is normally an error status. That is correct for interactive use, but a CI gate should make the intended result visible and preserve a real command failure. Put the following shell step in the same job that changes or validates the CodeQL version:
set -eu
if git grep -n -E 'SelfHostedQuery' -- \
.github/codeql custom-codeql; then
printf '%s\n' \
'Removed CodeQL Actions module found; review the custom query before upgrading.' >&2
exit 1
else
status=$?
if [ "$status" -eq 1 ]; then
printf '%s\n' 'No SelfHostedQuery import found in local custom query paths.'
else
printf '%s\n' 'Unable to scan the configured custom query paths.' >&2
exit "$status"
fi
fi
The if condition is deliberate. A matching git grep returns zero and enters the first branch, where the job prints the affected lines and stops. A no-match result returns one and enters the second branch, where the script explicitly accepts that one result. A status other than one remains an error. Do not write git grep ... || true; that would turn a bad path or an unavailable repository into the same apparent success as a clean scan.
If the repository does not always contain both directories, build the path list before calling git grep. In a POSIX shell, that can be a small loop that appends only directories that exist. Keep the list under source control so a future query-pack relocation updates the gate in the same change. A gate that silently scans no custom query path has no useful coverage.
Prove that the gate distinguishes the two cases
A successful no-match check is useful only if the gate would also reject the old import. Test both cases in a disposable repository or a temporary copy of the query directory. Do not add a deliberately broken QL file to the production branch merely to test CI.
The validation fixture used these two files:
// custom-queries/legacy.ql
import codeql.actions.security.SelfHostedQuery
from int x
select x
// custom-queries/current.ql
import codeql.actions.Workflow
from Workflow workflow
select workflow
Run the search against the directory, then against the file that does not contain the obsolete import:
git grep -n -E 'SelfHostedQuery' -- custom-queries
printf 'legacy fixture status: %s\n' "$?"
git grep -n -E 'SelfHostedQuery' -- custom-queries/current.ql
printf 'current fixture status: %s\n' "$?"
The isolated test printed the legacy import and returned status 0 for the directory search. The single-file search printed nothing and returned status 1. That paired result matters because it tests the shell contract the CI step depends on: the check is not simply a command that happens to be quiet on one tree.
The fixture is intentionally not a complete query test. It does not create an Actions database or assert a security result. Its job is to test the migration gate’s input and exit-status behavior. After the scan passes, compile the repository’s real custom query pack with the exact CodeQL version that the workflow will use. Compilation and a representative analysis are separate checks that can reveal unsupported imports beyond this one removed module.
Review the query’s intent before changing it
The removed module existed to identify workflows believed to run on self-hosted runners. The release note says runner labels cannot make that distinction reliably, so replacing the import with a similarly named local predicate would reproduce the same unreliable assumption. Start from what the query used the classification for.
For example, a query may have used the classification to escalate a finding around secrets, untrusted input, or privileged workflow steps. Review the workflow itself and the platform controls that establish the relevant boundary. The useful evidence may include the event trigger, permissions block, environment approval requirement, runner group, organization policy, or the identity allowed to register a runner. Those controls are more concrete than a label string such as self-hosted or linux.
Keep this review defensive and bounded. Do not expose a runner’s network placement, registration token, internal labels, or secrets in a query comment or CI log. A public custom query should describe the security property it checks, while private operational details remain in the access-controlled configuration that owns them.
Once the policy is clear, update the custom query to a supported model or retire it if it cannot make a trustworthy distinction. Then run three checks: the removed-import gate, codeql query compile for the real pack, and the existing CodeQL workflow against an authorized test repository. Record the CodeQL CLI and pack versions with the result. A changed alert count may reflect query improvements in the new release, so compare individual findings instead of treating a count difference as proof that protection was added or lost.
Keep the gate until the upgrade is routine
The source scan is deliberately small enough to run before every planned CodeQL upgrade. Leave it in place while repositories carry custom Actions queries, even after the immediate import has been removed. It provides a clear failure if an older query is copied back from another branch or internal template.
For broader library compatibility, add exact-version compilation of the real query pack to a disposable validation job. The text gate catches the documented API removal early; compilation catches the rest of the imports; a controlled analysis checks the query’s intended behavior. Keeping those stages separate makes an upgrade failure easier to attribute and avoids presenting a clean text search as a successful security analysis.