Local Fitness

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: read on the main CI file, which is good hygiene. Do that to the CodeQL workflow without restoring security-events: write at 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-scoped permissions block 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-severity unset and the action fails on low and above, so the first low-severity advisory in a transitive dependency turns into a red check on an unrelated PR. Decide the bar explicitly; high is 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

Changelog

  • release: 0.15.0 — chart line/calendar + public-share readiness (dev → main) (#66) (185b3ee)