CI Infrastructure — Overview

Purpose

The ci/ directory contains the Continuous Integration infrastructure for the Project Tick monorepo. It provides reproducible builds, automated code quality checks, commit message validation, pull request lifecycle management, and code ownership enforcement — all orchestrated through Nix expressions and JavaScript-based GitHub Actions scripts.

The CI system is designed around three core principles:

  1. Reproducibility — Pinned Nix dependencies ensure identical builds across environments
  2. Conventional Commits — Enforced commit message format for automated changelog generation
  3. Ownership-driven review — CODEOWNERS-style file ownership with automated review routing

Directory Structure

ci/
├── OWNERS                           # Code ownership file (CODEOWNERS format)
├── README.md                        # CI README with local testing instructions
├── default.nix                      # Nix CI entry point — treefmt, codeowners-validator, shell
├── pinned.json                      # Pinned Nixpkgs + treefmt-nix revisions (npins format)
├── update-pinned.sh                 # Script to update pinned.json via npins
├── supportedBranches.js             # Branch classification logic for CI decisions
├── codeowners-validator/            # Builds codeowners-validator from source (Go)
│   ├── default.nix                  # Nix derivation for codeowners-validator
│   ├── owners-file-name.patch       # Patch: custom OWNERS file path via OWNERS_FILE env var
│   └── permissions.patch            # Patch: remove write-access check (not needed for non-native CODEOWNERS)
└── github-script/                   # JavaScript CI scripts for GitHub Actions
    ├── run                          # CLI entry point for local testing (commander-based)
    ├── lint-commits.js              # Commit message linter (Conventional Commits)
    ├── prepare.js                   # PR preparation: mergeability, branch targeting, touched files
    ├── reviews.js                   # Review lifecycle: post, dismiss, minimize bot reviews
    ├── get-pr-commit-details.js     # Extract commit SHAs, subjects, changed paths via git
    ├── withRateLimit.js             # GitHub API rate limiting with Bottleneck
    ├── package.json                 # Node.js dependencies (@actions/core, @actions/github, bottleneck, commander)
    ├── package-lock.json            # Lockfile for reproducible npm installs
    ├── shell.nix                    # Nix dev shell for github-script (Node.js + gh CLI)
    ├── README.md                    # Local testing documentation
    ├── .editorconfig                # Editor configuration
    ├── .gitignore                   # Git ignore rules
    └── .npmrc                       # npm configuration

How CI Works End-to-End

1. Triggering

CI runs are triggered by GitHub Actions workflows (defined in .github/workflows/) when pull requests are opened, updated, or merged against supported branches. The supportedBranches.js module classifies branches to determine which checks to run.

2. Environment Setup

The CI environment is bootstrapped via ci/default.nix, which:

  • Reads pinned dependency revisions from ci/pinned.json
  • Fetches the exact Nixpkgs tarball at the pinned commit
  • Imports treefmt-nix for code formatting
  • Builds the codeowners-validator tool with Project Tick–specific patches
  • Exposes a development shell with all CI tools available
# ci/default.nix — entry point
let
  pinned = (builtins.fromJSON (builtins.readFile ./pinned.json)).pins;
in
{
  system ? builtins.currentSystem,
  nixpkgs ? null,
}:
let
  nixpkgs' =
    if nixpkgs == null then
      fetchTarball {
        inherit (pinned.nixpkgs) url;
        sha256 = pinned.nixpkgs.hash;
      }
    else
      nixpkgs;

  pkgs = import nixpkgs' {
    inherit system;
    config = { };
    overlays = [ ];
  };

3. Code Formatting (treefmt)

The default.nix configures treefmt-nix with multiple formatters:

Formatter Purpose Configuration
actionlint GitHub Actions workflow linting Enabled, no custom config
biome JavaScript/TypeScript formatting Single quotes, no semicolons, no JSON format
keep-sorted Sorted list enforcement Enabled, no custom config
nixfmt Nix expression formatting Uses pkgs.nixfmt
yamlfmt YAML formatting Retains line breaks
zizmor GitHub Actions security scanning Enabled, no custom config

Biome is configured with specific style rules:

programs.biome = {
  enable = true;
  validate.enable = false;
  settings.formatter = {
    useEditorconfig = true;
  };
  settings.javascript.formatter = {
    quoteStyle = "single";
    semicolons = "asNeeded";
  };
  settings.json.formatter.enabled = false;
};
settings.formatter.biome.excludes = [
  "*.min.js"
];

4. Commit Linting

When a PR is opened or updated, ci/github-script/lint-commits.js validates every commit message against the Conventional Commits specification. It checks:

  • Format: type(scope): subject
  • No fixup!, squash!, or amend! prefixes (must be rebased before merge)
  • No trailing period on subject line
  • Lowercase first letter in subject
  • Known scopes matching monorepo project directories

The supported types are:

const CONVENTIONAL_TYPES = [
  'build', 'chore', 'ci', 'docs', 'feat', 'fix',
  'perf', 'refactor', 'revert', 'style', 'test',
]

And the known scopes correspond to monorepo directories:

const KNOWN_SCOPES = [
  'archived', 'cgit', 'ci', 'cmark', 'corebinutils',
  'forgewrapper', 'genqrcode', 'hooks', 'images4docker',
  'json4cpp', 'libnbtplusplus', 'meshmc', 'meta', 'mnv',
  'neozip', 'tomlplusplus', 'repo', 'deps',
]

5. PR Preparation and Validation

The ci/github-script/prepare.js script handles PR lifecycle:

  1. Mergeability check — Polls GitHub's mergeability computation with exponential backoff (5s, 10s, 20s, 40s, 80s retries)
  2. Branch classification — Classifies base and head branches using supportedBranches.js
  3. Base branch suggestion — For WIP branches, computes the optimal base branch by comparing merge-base commit distances across master and all release branches
  4. Merge conflict detection — If the PR has conflicts, uses the head SHA directly; otherwise uses the merge commit SHA
  5. Touched file detection — Identifies which CI-relevant paths were modified:
    • ci — any file under ci/
    • pinnedci/pinned.json specifically
    • github — any file under .github/

6. Review Lifecycle Management

The ci/github-script/reviews.js module manages bot reviews:

  • postReview() — Posts or updates a review with a tracking comment tag (<!-- projt review key: <key>; resolved: false -->)
  • dismissReviews() — Dismisses, minimizes (marks as outdated), or resolves bot reviews when the underlying issue is fixed
  • Reviews are tagged with a reviewKey to allow multiple independent review concerns on the same PR

7. Rate Limiting

All GitHub API calls go through ci/github-script/withRateLimit.js, which uses the Bottleneck library for request throttling:

  • Read requests: controlled by a reservoir updated from the GitHub rate limit API
  • Write requests (POST, PUT, PATCH, DELETE): minimum 1 second between calls
  • The reservoir keeps 1000 spare requests for other concurrent jobs
  • Reservoir is refreshed every 60 seconds
  • Requests to github.com (not the API), /rate_limit, and /search/ endpoints bypass throttling

8. Code Ownership Validation

The ci/codeowners-validator/ builds a patched version of the codeowners-validator tool:

  • Fetched from GitHub at a specific pinned commit
  • Two patches applied:
    • owners-file-name.patch — Adds support for custom CODEOWNERS file path via OWNERS_FILE env var
    • permissions.patch — Removes the write-access permission check (not needed since Project Tick uses an OWNERS file rather than GitHub's native CODEOWNERS)

This validates the ci/OWNERS file against the actual repository structure and GitHub organization membership.


Component Interaction Flow

┌─────────────────────────────────────────┐
│           GitHub Actions Workflow        │
│         (.github/workflows/*.yml)        │
└──────────────┬──────────────────────────┘
               │ triggers
               ▼
┌──────────────────────────────────────────┐
│          ci/default.nix                   │
│  ┌─────────┐  ┌──────────────────────┐   │
│  │pinned.  │  │ treefmt-nix          │   │
│  │json     │──│ (formatting checks)  │   │
│  └─────────┘  └──────────────────────┘   │
│               ┌──────────────────────┐   │
│               │ codeowners-validator │   │
│               │ (OWNERS validation)  │   │
│               └──────────────────────┘   │
└──────────────┬───────────────────────────┘
               │ also triggers
               ▼
┌──────────────────────────────────────────┐
│       ci/github-script/                   │
│  ┌────────────────┐  ┌───────────────┐   │
│  │ prepare.js      │  │ lint-commits │   │
│  │ (PR validation) │  │ (commit msg) │   │
│  └───────┬────────┘  └──────┬────────┘   │
│          │                   │            │
│  ┌───────▼────────┐  ┌──────▼────────┐   │
│  │ reviews.js      │  │ supported    │   │
│  │ (bot reviews)   │  │ Branches.js  │   │
│  └───────┬────────┘  └───────────────┘   │
│          │                                │
│  ┌───────▼────────┐                      │
│  │ withRateLimit   │                      │
│  │ (API throttle)  │                      │
│  └────────────────┘                      │
└──────────────────────────────────────────┘

Key Design Decisions

Why Nix for CI?

Nix ensures that every CI run uses the exact same versions of tools, compilers, and libraries. The pinned.json file locks specific commits of Nixpkgs and treefmt-nix, eliminating "works on my machine" problems.

Why a custom OWNERS file?

GitHub's native CODEOWNERS has limitations:

  • Must be in .github/CODEOWNERS, CODEOWNERS, or docs/CODEOWNERS
  • Requires repository write access for all listed owners
  • Cannot be extended with custom validation

Project Tick uses ci/OWNERS with the same glob pattern syntax but adds:

  • Custom file path support (via the OWNERS_FILE environment variable)
  • No write-access requirement (via the permissions patch)
  • Integration with the codeowners-validator for structural validation

Why Bottleneck for rate limiting?

GitHub Actions can run many jobs in parallel, and each job makes API calls. Without throttling, a large CI run could exhaust the GitHub API rate limit (5000 requests/hour for authenticated requests). Bottleneck provides:

  • Concurrency control (1 concurrent request by default)
  • Reservoir-based rate limiting (dynamically updated from the API)
  • Separate throttling for mutative requests (1 second minimum between writes)

Why local testing support?

The ci/github-script/run CLI allows developers to test CI scripts locally before pushing. This accelerates development and reduces CI feedback loops:

cd ci/github-script
nix-shell     # sets up Node.js + dependencies
gh auth login # authenticate with GitHub
./run lint-commits YongDo-Hyun Project-Tick 123
./run prepare YongDo-Hyun Project-Tick 123

Pinned Dependencies

The CI system pins two external Nix sources:

Dependency Source Branch Purpose
nixpkgs github:NixOS/nixpkgs nixpkgs-unstable Base package set for CI tools
treefmt-nix github:numtide/treefmt-nix main Multi-formatter orchestrator

Pins are stored in ci/pinned.json in npins v5 format:

{
  "pins": {
    "nixpkgs": {
      "type": "Git",
      "repository": {
        "type": "GitHub",
        "owner": "NixOS",
        "repo": "nixpkgs"
      },
      "branch": "nixpkgs-unstable",
      "revision": "bde09022887110deb780067364a0818e89258968",
      "url": "https://github.com/NixOS/nixpkgs/archive/bde09022887110deb780067364a0818e89258968.tar.gz",
      "hash": "13mi187zpa4rw680qbwp7pmykjia8cra3nwvjqmsjba3qhlzif5l"
    },
    "treefmt-nix": {
      "type": "Git",
      "repository": {
        "type": "GitHub",
        "owner": "numtide",
        "repo": "treefmt-nix"
      },
      "branch": "main",
      "revision": "e96d59dff5c0d7fddb9d113ba108f03c3ef99eca",
      "url": "https://github.com/numtide/treefmt-nix/archive/e96d59dff5c0d7fddb9d113ba108f03c3ef99eca.tar.gz",
      "hash": "02gqyxila3ghw8gifq3mns639x86jcq079kvfvjm42mibx7z5fzb"
    }
  },
  "version": 5
}

To update pins:

cd ci/
./update-pinned.sh

This runs npins --lock-file pinned.json update to fetch the latest revisions.


Node.js Dependencies (github-script)

The ci/github-script/package.json declares:

{
  "private": true,
  "dependencies": {
    "@actions/core": "1.11.1",
    "@actions/github": "6.0.1",
    "bottleneck": "2.19.5",
    "commander": "14.0.3"
  }
}
Package Version Purpose
@actions/core 1.11.1 GitHub Actions core utilities (logging, outputs)
@actions/github 6.0.1 GitHub API client (Octokit wrapper)
bottleneck 2.19.5 Rate limiting / request throttling
commander 14.0.3 CLI argument parsing for local ./run tool

These versions are kept in sync with the actions/github-script action.


Nix Dev Shell

The ci/github-script/shell.nix provides a development environment for working on the CI scripts locally:

{
  system ? builtins.currentSystem,
  pkgs ? (import ../../ci { inherit system; }).pkgs,
}:

pkgs.callPackage (
  {
    gh,
    importNpmLock,
    mkShell,
    nodejs,
  }:
  mkShell {
    packages = [
      gh
      importNpmLock.hooks.linkNodeModulesHook
      nodejs
    ];

    npmDeps = importNpmLock.buildNodeModules {
      npmRoot = ./.;
      inherit nodejs;
    };
  }
) { }

This gives you:

  • nodejs — Node.js runtime
  • gh — GitHub CLI for authentication
  • importNpmLock.hooks.linkNodeModulesHook — Automatically links node_modules from the Nix store

Outputs Exposed by default.nix

The ci/default.nix exposes the following attributes:

Attribute Type Description
pkgs Nixpkgs The pinned Nixpkgs package set
fmt.shell Derivation Dev shell with treefmt formatter available
fmt.pkg Derivation The treefmt wrapper binary
fmt.check Derivation A check derivation that fails if formatting drifts
codeownersValidator Derivation Patched codeowners-validator binary
shell Derivation Combined CI dev shell (fmt + codeowners-validator)
rec {
  inherit pkgs fmt;
  codeownersValidator = pkgs.callPackage ./codeowners-validator { };

  shell = pkgs.mkShell {
    packages = [
      fmt.pkg
      codeownersValidator
    ];
  };
}

Integration with Root Flake

The root flake.nix provides:

  • Dev shells for all supported systems (aarch64-linux, x86_64-linux, etc.)
  • A formatter (nixfmt-rfc-style)
  • The CI default.nix is imported indirectly via the flake for Nix-based CI runs
{
  description = "Project Tick is a project dedicated to providing developers
    with ease of use and users with long-lasting software.";

  inputs = {
    nixpkgs.url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz";
  };
  ...
}

Summary of CI Checks

Check Tool / Script Scope
Code formatting treefmt (biome, nixfmt, yamlfmt, actionlint, zizmor) All source files
Commit message format lint-commits.js All commits in a PR
PR mergeability prepare.js Every PR
Base branch targeting prepare.js + supportedBranches.js WIP → development PRs
Code ownership validity codeowners-validator ci/OWNERS file
GitHub Actions security zizmor (via treefmt) .github/workflows/*.yml
Sorted list enforcement keep-sorted (via treefmt) Files with keep-sorted markers

Related Documentation

Was this handbook page helpful?

This page is part of the Project Tick Handbook, which is licensed under the Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license. View full license details.
Last updated: April 18, 2026