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:
- Reproducibility — Pinned Nix dependencies ensure identical builds across environments
- Conventional Commits — Enforced commit message format for automated changelog generation
- 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-nixfor code formatting - Builds the
codeowners-validatortool 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!, oramend!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:
- Mergeability check — Polls GitHub's mergeability computation with exponential backoff (5s, 10s, 20s, 40s, 80s retries)
- Branch classification — Classifies base and head branches using
supportedBranches.js - Base branch suggestion — For WIP branches, computes the optimal base branch by comparing
merge-base commit distances across
masterand all release branches - Merge conflict detection — If the PR has conflicts, uses the head SHA directly; otherwise uses the merge commit SHA
- Touched file detection — Identifies which CI-relevant paths were modified:
ci— any file underci/pinned—ci/pinned.jsonspecificallygithub— 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
reviewKeyto 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 viaOWNERS_FILEenv varpermissions.patch— Removes the write-access permission check (not needed since Project Tick uses anOWNERSfile rather than GitHub's nativeCODEOWNERS)
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, ordocs/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_FILEenvironment 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 runtimegh— GitHub CLI for authenticationimportNpmLock.hooks.linkNodeModulesHook— Automatically linksnode_modulesfrom 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.nixis 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
- Nix Infrastructure — Deep dive into the Nix expressions
- Commit Linting — Commit message conventions and validation rules
- PR Validation — Pull request checks and lifecycle management
- Branch Strategy — Branch naming, classification, and release branches
- CODEOWNERS — Ownership file format and validation
- Formatting — Code formatting configuration and tools
- Rate Limiting — GitHub API rate limiting strategy