Learn GitHub Actions - Automated Testing, Code Quality & Security Scanning
Episode 16 of 21

Learn GitHub Actions - Automated Testing, Code Quality & Security Scanning

Code that successfully builds isn't necessarily high quality and secure. In this episode we automate unit tests, integration tests, and coverage uploads to Codecov, then integrate ESLint, ShellCheck, and SonarQube to maintain quality. We also enable SAST with CodeQL and Dependabot so that vulnerabilities in code and dependencies are detected earlier.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Introduction

In episode 15 we managed self-hosted runners and understood when we need them. Now we enter the most crucial phase of CI/CD: proving that the code is truly worthy of release. A successful build only proves the code compiles; quality, security, and runtime behavior are still untested. A good production pipeline must answer four questions: do the functions work (test), how widely are they tested (coverage), is the code clean (static analysis), and are there vulnerabilities (security scanning).

In this episode we discuss:

  1. Automating unit tests, integration tests, and coverage uploads to Codecov or Coveralls.
  2. Code quality & static analysis with ESLint, ShellCheck, and SonarQube/SonarCloud.
  3. Native GitHub security scanning with CodeQL (SAST) and Dependabot.

Building Quality Gates in the Pipeline

Imagine the pipeline like a factory production line. Each station has the task of inspecting a product before handing it to the next station. If one station passes without inspecting, defective products will keep flowing all the way to the customer. The same goes for the pipeline: every job is an inspection station that stops the flow the moment it finds a problem.

The ideal order: lint → unit test → coverage → static analysis → security scan. All run when a pull request is created, so problems are found before the code enters main.

Automating Unit Tests, Integration Tests & Coverage

Setting Up a Measurable Test Script

The prerequisite is an npm script that produces a coverage report in a format readable by external tools. For a Node.js project with Vitest, for example:

NPMpackage.json - test script with coverage
{
  "scripts": {
    "test": "vitest run",
    "test:coverage": "vitest run --coverage"
  }
}

The test:coverage command generates a coverage/lcov.info file plus an HTML report. Separate unit tests (fast, isolated, no network) from integration tests (using a real database or HTTP) for faster feedback — developers shouldn't wait for slow integration tests on every small commit.

Workflow Test + Upload Coverage to Codecov

The following workflow runs tests then uploads the coverage report to Codecov, which displays badges and can be configured to fail if coverage drops below a threshold:

test.yml - unit & integration tests plus coverage
name: Test & Coverage
 
on:
  push:
    branches: [main]
  pull_request:
 
permissions:
  contents: read
 
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout kode
        uses: actions/checkout@v4
 
      - name: Setup Node.js 20
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
 
      - name: Install dependensi
        run: npm ci
 
      - name: Jalankan unit & integration test
        run: npm run test:coverage
 
      - name: Upload coverage ke Codecov
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: coverage/lcov.info
          fail_ci_if_error: true

A few important things:

  • npm ci performs a deterministic install from the lockfile — more reliable than npm install because the result is always identical.
  • fail_ci_if_error: true makes the workflow fail if the coverage upload errors, so you never silently lose reports.
  • The Codecov token is stored as a repository secret. For public repositories, Codecov also accepts uploads without a token on pull requests from contributors.
  • For Coveralls, the equivalent action is coverallsapp/github-action@v2 with the same files.

Tip

Set up branch protection on main that requires the test job to pass. That way tests and coverage become a mandatory gate before a pull request is merged.

Code Quality & Static Analysis

ESLint & ShellCheck

Linters catch errors a compiler won't: unused variables, wrong conditions, or dangerous patterns. A separate workflow makes the results clear per job:

lint.yml - ESLint and ShellCheck
name: Lint & Static Analysis
 
on:
  pull_request:
 
permissions:
  contents: read
 
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout kode
        uses: actions/checkout@v4
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
 
      - name: ESLint
        run: npx eslint . --max-warnings 0
 
  shellcheck:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout kode
        uses: actions/checkout@v4
 
      - name: Jalankan ShellCheck
        uses: ludeeus/action-shellcheck@master

The --max-warnings 0 flag makes lint fail even on a single warning — the right standard for teams that want discipline. The shellcheck job uses a community action to check all *.sh scripts in the repository.

SonarQube & SonarCloud

Linters check file by file; SonarQube adds cross-module analysis: code duplication, complexity, and reliability bugs. SonarCloud is its cloud version — free for open source projects and stores analysis results in the cloud without needing your own server:

SonarCloud scan for pull requests
name: SonarCloud Analysis
 
on:
  pull_request:
 
jobs:
  sonar:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout kode
        uses: actions/checkout@v4
 
      - name: Analisis SonarCloud
        uses: SonarSource/sonarcloud-scan-action@v5
        with:
          args: >
            -Dsonar.organization=my-org
            -Dsonar.projectKey=my-org_my-app
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

The scan results are posted as a comment on the pull request and counted as a check. If the quality gate in the SonarCloud dashboard fails, the pull request can be configured to fail.

Warning

Never write the Sonar token directly in a YAML file. All tokens are fetched through the secrets context, e.g., secrets.SONAR_TOKEN, not typed manually into the workflow.

Native GitHub Security Scanning

CodeQL — SAST from GitHub

CodeQL analyzes code statically (SAST) and looks for vulnerability patterns such as SQL injection, path traversal, or unsafe deserialization. Its official action needs three steps: init, autobuild, and analyze.

codeql.yml - CodeQL SAST
name: CodeQL Analysis
 
on:
  push:
    branches: [main]
  pull_request:
  schedule:
    - cron: '0 3 * * 1'
 
permissions:
  security-events: write
  contents: read
 
jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    timeout-minutes: 360
    strategy:
      fail-fast: false
      matrix:
        language: ['javascript-typescript']
    steps:
      - name: Checkout kode
        uses: actions/checkout@v4
 
      - name: Initialize CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: ${{ matrix.language }}
 
      - name: Autobuild
        uses: github/codeql-action/autobuild@v3
 
      - name: Perform analysis
        uses: github/codeql-action/analyze@v3

Important explanations:

  • security-events: write is needed so scan results can be written to the Security tab.
  • schedule with cron 0 3 * * 1 (every Monday 03:00 UTC) runs a periodic full scan; on pull requests only the delta of changes is analyzed.
  • autobuild guesses how to build the project; for languages it can't detect, replace it with a manual build step.

The results appear in the Security → Code scanning tab as alerts, complete with severity, location, and fix recommendations.

Dependabot — Vulnerability Scanning on Dependencies

Your code is safe, but its dependencies might not be. Dependabot monitors package managers (npm, pip, Maven, and others) and opens automatic pull requests when a version that fixes a CVE is available. Its configuration lives in .github/dependabot.yml:

dependabot.yml - automatic dependency updates
version: 2
updates:
  - package-ecosystem: npm
    directory: /
    schedule:
      interval: weekly
      day: monday
      time: '09:00'
      timezone: Asia/Jakarta
    open-pull-requests-limit: 10
    labels:
      - dependencies
      - security
    ignore:
      - dependency-name: '*'
        update-types: ['version-update:semver-major']

Dependabot distinguishes security updates (triggered by CVEs, high priority, not waiting for the schedule) from version updates (the routine schedule above). Both produce pull requests, so give them clear labels and require review before merging.

Conclusion

In this episode we built quality and security gates:

  • The test workflow runs unit & integration tests then uploads coverage to Codecov.
  • ESLint and ShellCheck catch style errors and small bugs before merge.
  • SonarQube/SonarCloud add cross-module analysis with a quality gate.
  • CodeQL scans for static vulnerabilities on every pull request and on schedule.
  • Dependabot opens automatic pull requests for problematic dependencies.

A pipeline that tests everything can feel frightening when an accident happens in production. In episode 17 we discuss environment protection rules & approval gateways — how to add manual approval and environment controls so production can't be deployed carelessly. See you there!

Learn GitHub Actions - Automated Testing, Code Quality & Security Scanning | Learn GitHub Actions