Wiring up CodeQL and dependency review before a repo goes public
Shipped
local-fitness v0.15.0 was the release that got a private repo ready to share: a cleaner terminal line chart, the frontend’s first tests, and a CI hardening pass driven by a readiness audit. The part of that audit worth walking through is the security tooling it added: a CodeQL workflow and a dependency review workflow, both free on public repos. While the repo was private, the CI gate only had to satisfy me. Making it public changes who relies on the code, and these two workflows raise the bar for close to zero effort. Here is how to wire them and prove they work.
Two exposures, two scanners
The two workflows cover different halves of the problem. CodeQL is GitHub’s static analysis engine; it treats your code like queryable data, hunts for patterns that map to real vulnerability classes (injection, broken auth, path traversal), and reports what it finds as code scanning alerts in the repo’s Security tab. Dependency review covers the other half: on every pull request that touches a manifest or lockfile, it diffs the dependency changes and can fail the check if a newly added package carries a known vulnerability, before that package ever lands. It complements Dependabot rather than duplicating it; Dependabot patches the vulnerable dependencies you already have, while dependency review stops new ones at the door.
Both are included for public repos, and per the dependency review docs, private repos only get the latter with a paid GitHub Code Security plan. That pricing line matters for the timing of this whole exercise, and it comes back in the gotchas.
The CodeQL workflow
One file, .github/workflows/codeql.yml. This is the shape that shipped in this release, generalized to a single main branch; set the language matrix to your own stack:
name: CodeQL
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: "27 4 * * 1" # weekly: re-checks idle code against new advisories
permissions:
contents: read
jobs:
analyze:
runs-on: ubuntu-latest
permissions:
security-events: write # required to upload alerts
actions: read
contents: read
strategy:
fail-fast: false
matrix:
language: [python, javascript-typescript]
steps:
- uses: actions/checkout@v7
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
language: ${{ matrix.language }}
queries: security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Analyze
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{ matrix.language }}"
Three choices in there are deliberate. The matrix scans both languages in this project (a Python backend and a TypeScript frontend) as separate jobs, and fail-fast: false keeps one language’s failure from cancelling the other’s results. The security-and-quality query suite is broader than the default; it adds maintainability findings on top of the security ones, which is the right trade when you are about to have readers. And the schedule trigger covers the code no push touches: CodeQL’s query packs keep gaining new checks after your files stop changing, and the weekly re-scan applies the current rules to code that has been idle for months.
The nested permissions block is load-bearing, not boilerplate. The CodeQL action’s own docs state that every advanced-setup code scanning workflow must have the security-events: write permission; without it the analysis runs and then fails at upload. More on that in the gotchas.
The dependency review workflow
The second file is smaller, .github/workflows/dependency-review.yml:
name: Dependency Review
on:
pull_request:
branches: [main]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Dependency Review
uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
It only triggers on pull_request, because the whole point is a diff: what does this PR add? The one knob worth setting on day one is fail-on-severity. Per the action’s configuration reference, the default threshold is low, meaning any newly introduced vulnerability at low severity or above fails the check. This release set it to high: a PR that adds a high or critical CVE cannot merge, while low-severity noise in a transitive dependency does not block routine work. Once the check is green in a few PRs, add it to branch protection and the rule enforces itself.
Verify both actually ran
Before pushing, parse the files locally; a YAML typo is the most common way a new workflow silently never registers. Any YAML parser works:
npx -y js-yaml .github/workflows/codeql.yml
npx -y js-yaml .github/workflows/dependency-review.yml
Each command prints the parsed document as JSON and exits nonzero on a syntax error, so a clean JSON dump for both files means GitHub will at least accept them.
After pushing, confirm the runs exist. On this repo:
gh run list --workflow codeql.yml --limit 2
completed success feat(agent): redesign brief PDF ... CodeQL dev push 1m10s
completed success feat(agent): redesign brief PDF ... CodeQL ... pull_request 1m13s
Then pull the findings themselves. This queries the same data the Security tab shows:
gh api repos/OWNER/REPO/code-scanning/alerts --paginate \
--jq '[.[].rule.severity] | group_by(.) | map({(.[0]): length}) | add'
Running that against this project today returns real numbers:
{"error":4,"note":23,"warning":9}
That is the honest expectation to set: the first full scan of a real codebase surfaces findings, and on this one the four error-severity alerts included two flagged SQL injection paths. That is the argument for doing all of this while the repo is still private. Going public with an untriaged Security tab means publishing the findings along with the code. Work through the error-severity alerts first, then flip.
Gotchas
- Least-privilege permissions can starve the upload. The same readiness audit that added these workflows also put a top-level
permissions: contents: readon the main CI file, which is good hygiene. Do that to the CodeQL workflow without restoringsecurity-events: writeat the job level and the symptom is confusing: the analysis succeeds, then the upload step fails with a “Resource not accessible by integration” style error. The escape is the job-scopedpermissionsblock in the workflow above; the top level stays read-only. - The default severity threshold will block PRs you do not care about. Leave
fail-on-severityunset and the action fails onlowand above, so the first low-severity advisory in a transitive dependency turns into a red check on an unrelated PR. Decide the bar explicitly;highis a defensible starting point for a solo project. - The weekly scan can silently stop. GitHub’s workflow trigger docs note that scheduled workflows run only from the default branch, and that on public repos they are automatically disabled after 60 days without repository activity. The symptom is nothing: no failure, no email, just no new scans. If a project goes quiet, check the Actions tab for the disabled banner and re-enable.
- Dependency review is inert on a private repo without a paid plan. This project added the workflow while still private, ahead of going public, which means the check could not do real work until the flip. If you stage your security setup the same way, push a small test PR that bumps a dependency right after going public and confirm the check reports, rather than assuming the green pre-flip runs proved anything.
Sources
- About code scanning with CodeQL — GitHub Docs — what CodeQL is and how findings surface as code scanning alerts.
- About dependency review — GitHub Docs — dependency review checks PR dependency changes; free for public repos, paid for private.
- dependency-review-action — GitHub — configuration reference;
fail-on-severitydefaults tolow. - codeql-action — GitHub — advanced-setup workflows require the
security-events: writepermission. - Events that trigger workflows — GitHub Docs — scheduled workflows run from the default branch and are disabled after 60 days of inactivity on public repos.
Changelog
- release: 0.15.0 — chart line/calendar + public-share readiness (dev → main) (#66) (185b3ee)