From bcf60f7db9683eaaf3777f0a78cd637be8e87926 Mon Sep 17 00:00:00 2001 From: Ajay Ganapathy Date: Sun, 14 Dec 2025 19:12:45 -0500 Subject: [PATCH 01/18] chore: whitelist gitignore Signed-off-by: Ajay Ganapathy --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ecad843 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +* +!.gitignore +!README.md +!CONTRIBUTE.md From 2f6b6fffee7727e3cedbb643e08a64eecb09a3e4 Mon Sep 17 00:00:00 2001 From: Ajay Ganapathy Date: Sun, 19 Oct 2025 21:28:49 -0400 Subject: [PATCH 02/18] chore: set up nix flake - use nix build .# to build a package - use nix and direnv to load dev shells Signed-off-by: Ajay Ganapathy --- .envrc | 1 + .gitignore | 8 +- flake.lock | 40 ++ flake.nix | 1327 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1375 insertions(+), 1 deletion(-) create mode 100644 .envrc create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index ecad843..5edd18e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,10 @@ * +!.config +!.config/** +!.envrc !.gitignore -!README.md !CONTRIBUTE.md +!flake.nix +!flake.lock +!LICENSE +!README.md diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..abef8e4 --- /dev/null +++ b/flake.lock @@ -0,0 +1,40 @@ +{ + "nodes": { + "flake-schemas": { + "locked": { + "lastModified": 1721999734, + "narHash": "sha256-G5CxYeJVm4lcEtaO87LKzOsVnWeTcHGKbKxNamNWgOw=", + "rev": "0a5c42297d870156d9c57d8f99e476b738dcd982", + "revCount": 75, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.1.5/0190ef2f-61e0-794b-ba14-e82f225e55e6/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/DeterminateSystems/flake-schemas/%2A" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1755615617, + "narHash": "sha256-HMwfAJBdrr8wXAkbGhtcby1zGFvs+StOp19xNsbqdOg=", + "rev": "20075955deac2583bb12f07151c2df830ef346b4", + "revCount": 846535, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.846535%2Brev-20075955deac2583bb12f07151c2df830ef346b4/0198c7b8-5e15-730c-959e-40cbb5fe01b3/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/NixOS/nixpkgs/0.1.%2A" + } + }, + "root": { + "inputs": { + "flake-schemas": "flake-schemas", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..bdbfa45 --- /dev/null +++ b/flake.nix @@ -0,0 +1,1327 @@ +# This flake was initially generated by fh, the CLI for FlakeHub (version 0.1.25) +# +# This flake sets up development environments for golang, typescript, nix, and kubernetes +# It also creates a `lint`, `lintChanged`, `build`, `buildChanged`, `runTest`, `runTestChanged`, `publishDryRun` and `publishDryRunChanged` command, and links them into your $PATH +# +# See: https://zero-to-nix.com/concepts/flakes/ +# +# A flake can have many sections. Different nix commands read +# different sections +# +# Core Nix built-ins: +# • apps - nix run +# • packages - nix build, nix shell +# • legacyPackages - nix build, nix shell +# • checks - nix flake check +# • devShells - nix develop +# • formatter - nix fmt +# • templates - nix flake init, nix flake new +# • overlays - Used by other flakes/nixpkgs +# +# Extended types - no direct nix commands, used by other tools: +# • hydraJobs - Hydra CI system +# • dockerImages - Docker build tools +# +# NixOS - used by nixos-rebuild, not nix CLI: +# • nixosConfigurations - nixos-rebuild +# • nixosModules - Imported by other flakes +# +# Home Manager - used by home-manager CLI: +# • homeConfigurations - home-manager switch +# • homeModules - Imported by other flakes +# +# nix-darwin - used by darwin-rebuild CLI: +# • darwinConfigurations - darwin-rebuild switch +# • darwinModules - Imported by other flakes +# ``` +# +# See: https://github.com/DeterminateSystems/flake-schemas/blob/main/flake.nix +# +{ + description = "Development tools for Projects Monorepo"; + + # Flake inputs + inputs = { + flake-schemas.url = "https://flakehub.com/f/DeterminateSystems/flake-schemas/*"; + + nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1.*"; + }; + + # Flake outputs that other flakes can use + outputs = {flake-schemas, ...}: { + # Schemas tell Nix about the structure of your flake's outputs + schemas = flake-schemas.schemas; + }; +} +# New to nix? Confused by the syntax of this flake? +# No problem! The following should answer most of +# your questions: +# +# ___ ____ __________ ___ ____ +# / \ / / /__ ____/ \ \ / / +# / \ / / / / \ \/ / +# / \ / / / \ / +# / /\ / / / / \ +# / / \ / ___/ /____ / / \ \ +# /___/ \__/ /__________/ /___/ \___\ +# +# .-------_ ,--------, ,--, ,--, .------. +# / ,---, : /__ ___/ / / / / / _ / +# / / / / / / / / / / \ \'-' +# / /___-' / / / / / / / \ \ +# / ,___,-'' / / / / / / .--. \ \ +# / / ___/ /___ / /___ / /____ / '_' / +# /___/ /_________/ /______/ _______/ \_______.' +# +# +# This is an abridged and opinionated version of +# (https://nixos.org/guides/nix-pills/) +# +# Nix constructs isolated, reproducible development environments. +# It is like a devcontainer, without the underlying linux. +# +# ┌─────────────────────┐ ┌─────────────────────┐ +# │ │ │ │ +# │ Bring Your Own │ │ Reproducible │ +# │ Platform │ │ Development │ +# │ │ │ Environment │ +# │ ┌─────────────────┼───────────┼─────────────────┐ │ +# │ │ │ │ │ │ +# │ │ │ Nix │ │ │ +# │ │ │ │ │ │ +# │ └─────────────────┼───────────┼─────────────────┘ │ +# │ │ │ │ +# │ │ │ │ +# └─────────────────────┘ └─────────────────────┘ +# +# Nix brings reproducibility to the platform you already have +# (e.g. aarch64 darwin) instead of forcing you to replace your +# platform with a linux VM. Nix makes it possible to build for +# your platform, or even cross compile for any other platform. +# +# Without nix, matching dependencies across systems becomes +# every developer's problem: +# +# "It works on my machine!" +# ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +# │ Developer A │ │ Developer B │ │ CI Server │ +# │ │ │ │ │ │ +# │ gcc 11.2 │ │ gcc 12.1 │ │ gcc 10.3 │ +# │ node 18.5 │ │ node 16.8 │ │ node 14.2 │ +# │ python 3.9 │ │ python 3.11 │ │ python 3.8 │ +# │ │ │ │ │ │ +# └─────────────────┘ └─────────────────┘ └─────────────────┘ +# ✅ Builds fine ❌ Build fails ⚠️ Tests fail +# +# You tell nix what dependencies you need, and you give nix +# your build scripts. Nix creates a custom environment with +# those exact tools and then runs your scripts: +# +# "Same dependencies = same results!" +# ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +# │ Developer A │ │ Developer B │ │ CI Server │ +# │ │ │ │ │ │ +# │ gcc 11.2 │ │ gcc 11.2 │ │ gcc 11.2 │ +# │ node 18.5 │ │ node 18.5 │ │ node 18.5 │ +# │ python 3.9 │ │ python 3.9 │ │ python 3.9 │ +# │ │ │ │ │ │ +# └─────────────────┘ └─────────────────┘ └─────────────────┘ +# ✅ Builds fine ✅ Builds fine ✅ Tests pass +# +# HOW IT WORKS: +# +# Nix is a package manager, like apt, homebrew or yum.Like +# most packages managers, it downloads packages from its online +# package repository http://www.nixpkgs.io. Unlike most package +# managers, it actually handles conflicting dependencies. +# +# Most package managers create ONE environment with ALL of the +# packages you install. If two packages conflict, you have to +# choose one or the other! Want to use a version of libMysql or +# openssl that isn't compatible with the other libraries in your +# OS? Good luck downloading and compiling the source yourself. +# +# If you have ever had to coax an ubuntu devcontainer into +# installing an out-of-release version of a library, you know +# how difficult this is. +# +# Traditional Package Managers - ONE shared environment: +# ┌─────────────────────────────────────────────────────────────────┐ +# │ System Environment │ +# │ │ +# │ gcc-11.2 nodejs-18 python-3.9 libssl-1.1 libmysql-8.0 │ +# └─────────────────────────────────────────────────────────────────┘ +# ❌ Project A needs libssl-3.0 → CONFLICT! +# ❌ Project B needs python-3.11 → CONFLICT! +# ❌ Project C needs gcc-12 → CONFLICT! +# +# Nix sidesteps this problem entirely, by creating entirely +# separate environments for every project. Everything is in-release +# because each environment IS its own release! You can install as +# many versions of a library as you want, because nix automatically +# isolates packages between environments. +# +# ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +# │ Project A │ │ Project B │ │ Project C │ +# │ │ │ │ │ │ +# │ gcc-11.2 │ │ gcc-11.2 │ │ gcc-12 │ +# │ nodejs-18 │ │ nodejs-18 │ │ nodejs-18 │ +# │ python-3.9 │ │ python-3.11 │ │ python-3.9 │ +# │ libssl-1.1 │ │ libssl-1.1 │ │ libssl-3.0 │ +# │ libmysql-8.0 │ │ libmysql-8.0 │ │ libmysql-8.0 │ +# │ │ │ │ │ │ +# └─────────────────┘ └─────────────────┘ └─────────────────┘ +# +# ✅ No conflicts ✅ No conflicts ✅ No conflicts +# +# +# +# HOW TO USE NIX: +# +# To give nix your build scripts and dependencies, create a +# flake.nix, just like this one. A flake is a configuration file +# that constructs isolated environments for building projects. +# +# To define an isolated environment, create a `package` inside +# a nix flake. +# +# ```nix +# { +# inputs.flake-schemas.url = "https://flakehub.com/f/DeterminateSystems/flake-schemas/*"; +# +# outputs = { flake-schemas, nixpkgs }: +# let +# supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; +# forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f { +# pkgs = import nixpkgs { inherit system; }; +# }); +# in { +# schemas = flake-schemas.schemas; +# +# # Core Nix built-ins +# packages = forEachSupportedSystem ({ pkgs }: { ... }); +# ^^^ +# build scripts go here +# }; +# } +# ``` +# +# In this example, nix creates "default" package. This package +# constructs an environment that includes nodeJS 22 and npm. +# Then, it runs `npm run build`. To run this script, `nix build .#` +# +# ```nix +# { +# outputs = { flake-schemas, nixpkgs }: +# let +# supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; +# forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f { +# pkgs = import nixpkgs { inherit system; }; +# }); +# in { +# schemas = flake-schemas.schemas; +# +# packages = forEachSupportedSystem ({ pkgs }: { +# # Example: A Node.js project that runs 'npm build' +# default = pkgs.stdenv.mkDerivation { +# pname = "my-node-app"; +# version = "1.0.0"; +# +# src = ./.; +# +# buildInputs = [ pkgs.nodejs_22 ]; +# +# buildPhase = '' +# npm install +# npm run build +# ''; +# +# installPhase = '' +# mkdir -p $out +# cp -r dist/* $out/ +# ''; +# }; +# }); +# }; +# } +# ``` +# .---------------------, .-------------. +# | nix build .#default -----> nodejs 22 | +# '---------------------' | npm | .---------------. +# | -------> npm run build | +# '-------------' '---------------' +# +# Use the pkgs.stdenv.mkDerivation utility to specify the +# build tools and build scripts for your project: +# +# ```nix +# my-package = pkgs.stdenv.mkDerivation { +# # Package metadata +# pname = "my-package-name"; +# version = "1.0.0"; +# src = ./.; # Source code location +# +# # Build tools go in buildInputs +# buildInputs = [ pkgs.nodejs_22 pkgs.cmake pkgs.python311 ]; +# ^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^ +# Add any tools your build needs +# +# # Build commands go in buildPhase +# buildPhase = '' +# npm install +# npm run build +# # ^^^^^^^^^^^ +# # Any shell commands you need +# ''; +# +# # Installation commands go in installPhase +# installPhase = '' +# mkdir -p $out # $out is the output directory +# cp -r dist/* $out/ # Copy your built files here +# ''; +# }; +# ``` +# +# That's it! Three simple parts: +# 1. buildInputs = [ tools you need ] +# 2. buildPhase = '' commands to build '' +# 3. installPhase = '' copy results to $out '' +# +# Nix build creates a result/ folder in the Current +# working directory, and places the built artifacts +# inside it: +# +# Current Working Directory +# | +# |-- flake.nix +# '-- result <- symlink to built output in nix store +# +# +# pkgs.stdenv.mkDerivation is the fundamental building +# block of Nix. It creates isolated environments where your +# builds run safely. +# +# ORCHESTRATING BUILD SCRIPTS WITH NIX: +# +# Nix doesn't replace your existing build tools. It orchestrates them. +# +# Nix can even compose several build scripts together. This is +# the most powerful feature of Nix. +# +# In this example a JS project composes a go project, +# which composes a c project. +# +# +# ```nix +# packages = forEachSupportedSystem ({ pkgs }: { +# # Step 1: Build a C library with cmake +# example-c-project = pkgs.stdenv.mkDerivation { +# pname = "example-c-lib"; +# version = "1.0.0"; +# src = ./c-src; +# +# buildInputs = [ pkgs.cmake ]; +# +# buildPhase = '' +# cmake . +# make +# ''; +# +# installPhase = '' +# mkdir -p $out/lib $out/include +# cp libexample.so $out/lib/ +# cp example.h $out/include/ +# ''; +# }; +# +# # Step 2: Build Go binary that uses the C library +# example-go-project = pkgs.stdenv.mkDerivation { +# pname = "example-go-app"; +# version = "1.0.0"; +# src = ./go-src; +# +# buildInputs = [ pkgs.go ]; +# # Include the C library as a dependency +# propagatedBuildInputs = [ packages.example-c-project ]; +# +# buildPhase = '' +# export CGO_CFLAGS="-I${packages.example-c-project}/include" +# export CGO_LDFLAGS="-L${packages.example-c-project}/lib -lexample" +# go build -o myapp main.go +# ''; +# +# installPhase = '' +# mkdir -p $out/bin +# cp myapp $out/bin/ +# ''; +# }; +# +# # Step 3: JS project that copies Go binary as an asset +# example-js-project = pkgs.stdenv.mkDerivation { +# pname = "example-js-app"; +# version = "1.0.0"; +# src = ./js-src; +# +# buildInputs = [ pkgs.nodejs_22 ]; +# # Include the Go binary as a dependency +# propagatedBuildInputs = [ packages.example-go-project ]; +# +# buildPhase = '' +# # Copy Go binary into assets +# mkdir -p assets +# cp ${packages.example-go-project}/bin/myapp assets/ +# +# npm install +# npm run build +# ''; +# +# installPhase = '' +# mkdir -p $out +# cp -r dist/* $out/ +# # Go binary is now bundled in the JS app +# ''; +# }; +# }); +# ``` +# +# Build dependency DAG when you run: nix build .#example-js-project +# +# .--------------------------. +# | nix build .#example-js | ← User command +# '--------------------------' +# | +# v +# .--------------------------. .--------------------. +# | JS Environment | | example-go-project | ← Dependency +# | nodejs_22 + npm |<---| (binary artifact) | +# | assets/myapp copied | '--------------------' +# | | | | +# | v | v +# | .-------------. | .--------------------------. +# | | npm install | | | nix build .#example-go | ← Recursive build +# | | npm run build | '--------------------------' +# | '-------------' | | +# '--------------------------' v +# .--------------------. .--------------------. +# | Go Environment | | example-c-project | ← Dependency +# | go + cgo flags |<---| (lib + headers) | +# | CGO_CFLAGS set | '--------------------' +# | | | | +# | v | v +# | .-------------. | .--------------------------. +# | | go build | | | nix build .#example-c | ← Recursive build +# | '-------------' | '--------------------------' +# '--------------------' | +# v +# .-------------------. +# | C Environment | +# | cmake | +# | | | +# | v | +# | .-------------. | +# | | cmake . | | +# | | make | | +# | '-------------' | +# '-------------------' +# +# Each build step runs in isolation, and references the outputs +# of its dependencies. +# +# If you want to build the Go and C project, without +# building the JS project `nix build .#example-go-project` +# +# If you want to build the C project, without building +# the Go and JS project, you can `nix build .#example-c-project` +# +# DEBUGGING NIX BUILD +# +# use `nix build --log-format raw .#name-of-pkg` to print +# build progress and errors +# +# use `nix build --rebuild .#name-of-pkg` to delete the cached +# build artifact and rebuild from scratch +# +# +# LOADING BUILD TOOLS INTO YOUR SHELL WITH NIX: +# +# Many flakes also contain devShells. DevShells install +# dependencies in your $PATH. They make it possible +# to run arbitrary commands, with the same build tools +# you use in your build scripts. +# +# ```nix +# { +# inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; +# +# outputs = { nixpkgs }: +# let +# supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; +# forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f { +# pkgs = import nixpkgs { inherit system; }; +# }); +# in { +# devShells = forEachSupportedSystem ({ pkgs }: { +# default = pkgs.mkShell { +# buildInputs = [ pkgs.nodejs_22 ]; +# }; +# }); +# }; +# } +# ``` +# In this example, nix creates a shell with nodeJS 22 +# If your $PATH contains a different version of node, +# nix will temporarily shadow it with nodeJS 22. You +# can use this version of node, without installing it +# globally and managing it with node version manager. +# +# $PATH = /nix/store/abc123-nodejs-22/bin:/usr/local/bin:/usr/bin +# ↑ ↑ ↑ +# │ │ │ +# nix nodejs 22 local node system node +# (takes priority) (shadowed) (shadowed) +# +# +# +# +# +# MAKING CROSS-PLATFORM DEVELOPMENT ENVIRONMENTS, WITH NIX +# +# Nix flakes work across platforms. the `nixpkgs.lib.genAttrs` +# utility splits a flake into platform-specific variants. +# Each variant downloads the build of the dependencies +# for its respective operating system and processor +# architecture. +# +# ```nix +# { +# inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; +# +# outputs = { nixpkgs }: +# let +# supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; +# forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f { +# pkgs = import nixpkgs { inherit system; }; +# # ^^^ +# # use builds of packages that are specific to a given system +# }); +# in { +# } +# ``` +# +# ┌─────────────────────┐ ┌─────────────────────┐ +# │ Systems List │ │ Platform-Specific │ +# │ │ │ Packages │ +# │ ["x86_64-linux" │ ┌─────────────────┐ │ │ +# │ "aarch64-linux" │──▶ nixpkgs.lib. ──▶│ x86_64-linux-pkgs │ +# │ "x86_64-darwin" │ │ genAttrs │ │ aarch64-linux-pkgs │ +# │ "aarch64-darwin"] │ │ │ │ x86_64-darwin-pkgs │ +# │ │ └─────────────────┘ │ aarch64-darwin-pkgs │ +# └─────────────────────┘ └─────────────────────┘ +# +# While this technically creates multiple, slightly different +# environments - depending on the machine's underlying os and +# architecture - all machines of the same OS and arch will +# build using the exact same environment. +# +# the forEachSupportedSystem expression runs +# nixpkgs.lib.genAttrs. If your build scripts are +# platform-specific, you can use it to customize +# them for each platform and architecture. +# +# This example sets different environment variables +# based on the platform: +# +# - BUILD_TARGET: "linux" or "darwin" +# - BUILD_ARCH: "arm64" or "x64" +# +# ```nix +# { +# inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; +# +# outputs = { nixpkgs }: +# let +# supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; +# forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f { +# pkgs = import nixpkgs { inherit system; }; +# }); +# in { +# packages = forEachSupportedSystem ({ pkgs }: { +# default = pkgs.stdenv.mkDerivation { +# pname = "platform-aware-app"; +# version = "1.0.0"; +# +# src = ./.; +# +# buildInputs = [ pkgs.nodejs_22 ]; +# +# buildPhase = '' +# export BUILD_TARGET=${if pkgs.stdenv.isLinux then "linux" else "darwin"} +# export BUILD_ARCH=${if pkgs.stdenv.isAarch64 then "arm64" else "x64"} +# export NODE_ENV=production +# +# echo "Building for $BUILD_TARGET-$BUILD_ARCH" +# npm install +# npm run build +# ''; +# +# installPhase = '' +# mkdir -p $out +# cp -r dist/* $out/ +# ''; +# }; +# }); +# }; +# } +# ``` +# +# the forEachSupportedSystem function lets you moves messy +# platform-detection logic out of your build scripts and +# into the nix flake. +# +# WITHOUT forEachSupportedSystem: +# ┌─────────────────────────────┐ +# │ Build Script │ +# │ │ +# │ if os.platform() == "linux":│ +# │ TARGET = "linux" │ +# │ elif os.platform() =="win32"│ +# │ TARGET = "windows" │ +# │ elif os.platform()=="darwin"│ +# │ TARGET = "macos" │ +# │ │ +# │ if arch == "arm64": │ +# │ ARCH = "arm64" │ +# │ else: │ +# │ ARCH = "x64" │ +# │ │ +# │ npm run build │ +# └─────────────────────────────┘ +# +# WITH forEachSupportedSystem: +# ┌─────────────────────────────┐ ┌─────────────────────────────┐ +# │ Nix Flake │ │ Build Script │ +# │ │ │ │ +# │ BUILD_TARGET = if isLinux ────▶│ # Clean, simple: │ +# │ then "linux" │ │ npm run build │ +# │ else "darwin" │ │ │ +# │ │ │ # Uses $BUILD_TARGET and │ +# │ BUILD_ARCH = if isAarch64 │ │ # $BUILD_ARCH from nix │ +# │ then "arm64" │ │ │ +# │ else "x64" │ │ │ +# └─────────────────────────────┘ └─────────────────────────────┘ +# Platform logic lives here Platform logic removed! +# +# +# +# +# WRITING YOUR OWN UTILITY FUNCTIONS, WITH NIX +# +# Nix flakes are far more powerful than package.json +# pyproject.toml or make files, because nix is a +# turing-complete, purely functional scripting language: +# +# in fact, lib.system.genAttrs and pkgs.stdenv.mkDerivation +# are both scripts, written in nix! +# +# script | source +# --------------------------|------------------ +# pkgs.stdenv.mkDerivation | https://github.com/NixOS/nixpkgs/blob/master/pkgs/stdenv/generic/make-derivation.nix +# lib.system.genAttrs | https://github.com/NixOS/nixpkgs/blob/master/lib/system.nix +# +# Nix scripts compose these utilities into larger +# scripts that build everything from binaries +# to operating system images. +# +# to make a nix script, create a file that ends +# in .nix, and then write exactly one expression. +# An expression is anything that can be assigned +# to a variable. +# +# good.nix bad.nix +# ________________ ________________ +# / | / | +# | 42 | | 42 | +# | | | 42 | +# | | | 42 | +# | | | | +# | | | | +# | | | | +# |________________| |________________| +# nix eval good.nix A .nix file cannot +# returns 42 contain more than +# one expression. +# This contains 3. +# +# good.nix bad.nix +# ________________ ________________ +# / | / | +# | { | | x = x + y | +# | x = x: x + y;| | y = 42 | +# | y = 42; | | | +# | } | | | +# | | | | +# | | | | +# |________________| |________________| +# nix eval good.nix A .nix file cannot +# returns contain more than +# { one expression. +# x = x: x + y; This contains 2. +# y = 42; +# } +# +# +# If you need to declare multiple variables for an +# expression, wrap them in a "let...in" clause +# +# good.nix bad.nix +# ________________ ________________ +# / | / | +# | let | | x = 42; | +# | x = 42; | | y = 10; | +# | y = 10; | | x + y | +# | in | | | +# | x + y | | | +# | | | | +# |________________| |________________| +# evaluates to 42 contain more than +# +# +# if you need to pass one or more variables into +# an expression, return a function +# +# +# nix variables can be of the following types +# +# type example +# +# integer x = 3 +# +# floating point w = 3.14 +# number x = 2.5 * 2 <- a float * an int will +# always be a float, even +# if it is equal to an int. +# This is because nix +# preserves floating point +# precision +# y = 3 + 2.5 <- an integer + a float is a +# float +# z = 3/ 2 <- an integer that does not +# -----------^---------- divide evenly is a float +# You have to leave a +# space after / for nix +# to recognize it as a +# division operator +# +# boolean u = false +# v = true +# w = 1 < 2 <- evaluates to true +# x = 3 > 4 <- evaluates to false +# y = "foo" == "foo" <- evaluates to true +# x = "foo" != "bar" <- evaluates to true +# z = true && false <- evaluates to false +# a = true || false <- evaluates to true +# b = true && false || true <- evaluates to true +# +# string y = "foo" +# z = "Hello ${42}" # evaluates to "Hello 42" +# +# path z = path/to/file <- nix recognizes path +# as a primitive type +# to make locating +# files in the nix +# store easier +# +# list x = [ "foo" 3 3.14 ] +# ----------^--------- +# items in a list are +# separated by a space +# +# y = [ +# 1 +# 2 +# [ 3 4 5 ] <- lists can contain +# ] other lists +# +# z = [ +# "hello" +# { <- lists can contain +# a = 1; attribute sets +# b = 2; (a.k.a maps) +# } +# 42 +# ] +# +# +# attribute set z = { a = 1; b = 2; } +# (a.k.a map) ----------^--------- +# items in a map are +# followed by a semi- +# colon +# +# x = { +# a = 1; +# b = [ 1 2 3 ]; <- attribute sets can +# c = "hello"; contain members +# } that are lists +# +# y = { +# a = 1; +# b = { <- attribute sets can +# x = 10; contain members +# y = 20; that are attribute +# }; sets +# c = "world"; +# } +# +# function f = x: x + 1 <- takes x as input and +# returns x + 1 +# g = x: y: x + y <- takes x as input and returns +# a function that takes y as +# input, where x is already +# set in the returned function. +# in functional programming, +# this is known as "currying" +# h = { a, b, ... }: a + b <- takes an attribute set as +# ----------^---------- an input and destructures it +# you must use ... to +# extract a and b from +# the attribute set. If +# you omit it, then nix +# will error if the +# attribute set has any +# members other than a +# and b +# +# +# to make a nix script, create a file that ends in .nix, and then write exactly one expression. +# An expression is anything that can be assigned to a variable. +# +# good.nix bad.nix +# ____________________ ____________________ +# / | / | +# | let | | let | +# | fx = x: y: x * y | | fx = x y: x * y | +# | in | | in | +# | fx 3 2 | | fx 3 2 | +# |_-_-_-_-_-_-_-_-_-_-| |_-_-_-_-_-_-_-_-_-_-| +# evaluates to 6 A function can only +# accept one argument +# per invocation. +# +# currying takes the first argument, and returns a +# function that takes the second argument +# +# fx 3 2 +# -^---------------- +# 3 -> x: y: x * y 1. fx receives 3, and returns +# 3: y: 3 * y an anonymous function +# +# 2 -> y: 3 * y 2. the anonymous function +# 2: 3 * 2 receives 2 and returns +# 6 the answer +# +# if you don't want to curry a function that takes multiple +# arguments, you can pass them all at once in a single +# attribute set: +# +# let +# f = { x, y, z }: x + y + z; +# in +# f { x = 1; y = 2; z = 3; } <- evaluates to 6 +# ----^------------------------ +# all arguments are passed at +# once in a single attribute +# set +# +# +# use () to immediately evaluate an expression +# instead of assigning it to a variable: +# +# foo.nix +# _________________________ +# / | +# | y = [ | +# | (import bar.nix 1) <------ bar.nix +# | (import bar.nix 2) | ____________ +# | (import bar.nix 3) | / | +# | ]; | | x: x * 2 | +# |_-_-_-_-_-_-_-_-_-_-_-_-_| |_-_-_-_-_-_-| +# y evaluates to [ 2 4 6 ] +# +# the parentheses cause each import +# to be evaluated immediately with +# its argument +# +# +# compose nix scripts, with the import keyword +# to import a nix script into another, use the import keyword: +# +# foo.nix +# ______________________ +# / | +# | y = import bar.nix <-|--- bar.nix +# | | ____________ +# | | / | +# | | | 42 | +# | | |_-_-_-_-_-_-| +# |_-_-_-_-_-_-_-_-_-_-_-| +# evaluates to y = 42 +# +# +# use a function by passing arguments into it +# let +# f = x y z: x + y + z; +# in +# f 1 2 3 <- evaluates to 6 +# ---^-------------------- +# arguments are separated +# by spaces and passed in +# following the name of +# the function +# +# +# pass variables into a nix script, by returning +# a function that uses the variables +# +# foo.nix +# _________________________ +# / | +# | y = import bar.nix { <------ bar.nix +# | a = 1; | _________________ +# | b = 2; | / | +# | } | | { a, b }: a + b | +# | | |_-_-_-_-_-_-_-_-_| +# |_-_-_-_-_-_-_-_-_-_-_-_-_| +# evaluates to y = 3 +# +# +# nix has several builtins for iterating through lists +# and attrsets (attribute sets, also known as maps) +# +# https://nix.dev/manual/nix/2.28/language/builtins.html +# +# LISTS: +# Lists are ordered collections of values, written with square brackets +# and space-separated items: +# myList = [ "a" "b" "c" 1 2 3 ] +# mixedList = [ { name = "Alice"; } [ 1 2 ] "hello" ] +# +# here's how Nix lists are similar to lists in other languages: +# +# Python list: +# _______________ +# / | +# | my_list = [ | +# | "a", | +# | "b", | +# | 1, | +# | 2 | +# | ] | +# |_______________| +# +# JavaScript list: +# _______________ +# / | +# | const myList = [ +# | "a", | +# | "b", | +# | 1, | +# | 2 | +# | ]; | +# |_______________| +# +# Go list: +# _______________ +# / | +# | myList := []interface{}{ +# | "a", | +# | "b", | +# | 1, | +# | 2, | +# | } | +# |_______________| +# +# Nix list: +# _______________ +# / | +# | myList = [ | +# | "a" | +# | "b" | +# | 1 | +# | 2 | +# | ] | +# |_______________| +# +# Key differences in Nix: +# - No commas between elements (space-separated) +# Nix list: +# _______________ +# / | +# | myList = [ | +# | "a" | +# | "b" | <- no commas here +# | 1 | <- or here +# | 2 | <- or here +# | ] | +# |_______________| +# ----^----------- +# space-separated elements +# instead of comma-separated +# +# - No quotes around variable names when referencing them +# Variable reference: +# _______________ +# / | +# | let | +# | name = "Alice"; +# | greeting = name; <- no quotes around 'name' +# | in | +# | greeting | +# |_______________| +# --------^------- +# variable name used directly +# (would be "name" in other languages) +# +# - Can contain any mix of types without declaration +# Mixed types: +# _______________ +# / | +# | mixed = [ | +# | "string" | <- string +# | 42 | <- integer +# | true | <- boolean +# | { a = 1; } | <- attrset +# | ] | +# |_______________| +# ----^----------- +# no type declarations needed +# +# Nix also has attribute sets, which are knows as maps +# or objects in other languages. +# +# Attribute sets are key-value collections, written with curly braces +# and semicolon-separated key = value pairs: +# myAttrs = { name = "John"; age = 30; active = true; } +# nestedAttrs = { +# user = { name = "Jane"; email = "jane@example.com"; }; +# settings = { theme = "dark"; lang = "en"; }; +# } +# +# Nix attrsets are similar to dictionaries, objects and maps +# in other languages +# +# Python dict: +# ______________ +# / | +# | my_dict = { | +# | "name": "John", +# | "age": 30, | +# | "active": True +# | } | +# |______________| +# +# JavaScript object: +# ______________ +# / | +# | const myObj = { +# | name: "John", +# | age: 30, +# | active: true +# | }; | +# |______________| +# +# Go map: +# ______________ +# / | +# | myMap := map[string]interface{}{ +# | "name": "John", +# | "age": 30, +# | "active": true, +# | } | +# |______________| +# +# Nix attrset: +# ______________ +# / | +# | myAttrs = { | +# | name = "John"; +# | age = 30; +# | active = true; +# | } | +# |______________| +# +# Key differences in Nix: +# - Semicolons instead of commas between key-value pairs +# Nix attrset: +# _______________ +# / | +# | myAttrs = { | +# | name = "John"; <- semicolon here +# | age = 30; <- and here +# | active = true; <- and here +# | } | +# |_______________| +# --------^------- +# semicolons separate pairs +# (not commas like other languages) +# +# - Keys don't need quotes (unless they contain special characters) +# Key syntax: +# _______________ +# / | +# | attrs = { | +# | name = "John"; <- no quotes around 'name' +# | "user-id" = 123; <- quotes needed for hyphens +# | firstName = "Jane"; <- camelCase works +# | } | +# |_______________| +# ----^----------- +# simple keys don't need quotes +# (unlike JSON or some other formats) +# +# - Use = instead of : for key-value assignment +# Assignment operator: +# _______________ +# / | +# | myAttrs = { | +# | name = "John"; <- equals sign +# | age = 30; <- not colon +# | } | +# |_______________| +# -------^-------- +# equals for assignment +# (colon is used elsewhere in Nix) +# +# - Access with dot notation: myAttrs.name or bracket notation: myAttrs."name" +# Attribute access: +# _______________ +# / | +# | let | +# | attrs = { name = "Alice"; }; +# | getName = attrs.name; <- dot notation +# | getQuoted = attrs."name"; <- bracket notation +# | in getName | +# |_______________| +# ---------^------ +# both access methods work +# (brackets needed for special chars) +# +# +# Nix provides several builtins for iterating over maps +# and lists. +# +# ESSENTIAL ITERATION FUNCTIONS: +# The three most important functions you need to know: +# +# 1. builtins.map - Transform each element in a list +# Transform function: +# ___________________ +# / | +# | builtins.map | +# | (x: x * 2) [ | +# | 1 | +# | 2 | +# | 3 | +# | ] | +# |___________________| +# --------^-------- +# returns [ 2 4 6 ] +# +# String template function: +# _______________________ +# / | +# | builtins.map | +# | (name: "Hello | +# | ${name}") [ | +# | "Alice" | +# | "Bob" | +# | ] | +# |_______________________| +# ----------^---------- +# returns [ "Hello Alice" "Hello Bob" ] +# +# 2. builtins.filter - Keep only elements that match a condition +# Numeric filter: +# __________________ +# / | +# | builtins.filter | +# | (x: x > 5) [ | +# | 1 | +# | 10 | +# | 3 | +# | 8 | +# | 2 | +# | ] | +# |__________________| +# --------^-------- +# returns [ 10 8 ] +# +# String filter: +# ____________________ +# / | +# | builtins.filter | +# | (s: s != "") [ | +# | "a" | +# | "" | +# | "b" | +# | "" | +# | "c" | +# | ] | +# |____________________| +# ---------^--------- +# returns [ "a" "b" "c" ] +# +# 3. builtins.concatMap - Map then flatten (very common pattern) +# Duplicate function: +# ____________________ +# / | +# | builtins.concatMap | +# | (x: [ x x ]) [ | +# | 1 | +# | 2 | +# | 3 | +# | ] | +# |____________________| +# ---------^--------- +# returns [ 1 1 2 2 3 3 ] +# +# Template expansion: +# ____________________ +# / | +# | builtins.concatMap | +# | (name: [ | +# | "Mr. ${name}" | +# | "Ms. ${name}" | +# | ]) [ | +# | "Smith" | +# | "Jones" | +# | ] | +# |____________________| +# ---------^--------- +# returns [ "Mr. Smith" "Ms. Smith" "Mr. Jones" "Ms. Jones" ] +# +# Cross-language equivalents: +# +# | Language | Map | Filter | Concat Map | +# |------------|------------------|------------------|----------------------| +# | JavaScript | Array.map() | Array.filter() | Array.flatMap() | +# | Python | map() / [...] | filter() / [...] | itertools.chain() | +# | Nix | builtins.map | builtins.filter | builtins.concatMap | +# +# All iteration functions available in Nix: +# +# Lists: +# • builtins.map - Transform each element +# • builtins.filter - Keep elements matching condition +# • builtins.foldl' - Reduce list from left (recommended) +# • builtins.foldr - Reduce list from right +# • builtins.length - Get list length +# • builtins.elemAt - Get element at index +# • builtins.head - Get first element +# • builtins.tail - Get all elements except first +# • builtins.sort - Sort list with comparison function +# • builtins.groupBy - Group elements by key function +# • builtins.partition - Split list into two based on predicate +# +# Attribute Sets (objects/maps): +# • builtins.mapAttrs - Transform each value +# • builtins.filterAttrs - Keep key-value pairs matching condition +# • builtins.attrNames - Get all keys as list +# • builtins.attrValues - Get all values as list +# • builtins.hasAttr - Check if key exists +# • builtins.getAttr - Get value by key +# • builtins.removeAttrs - Remove specified keys +# • builtins.intersectAttrs - Keep only common keys +# +# Conversion: +# • builtins.listToAttrs - Convert list to attribute set +# • builtins.zipAttrsWith - Merge multiple attribute sets +# +# Documentation: https://nixos.org/manual/nix/stable/language/builtins.html +# +# +# WRITING FILES AND BUILD SCRIPTS WITH NIX: +# +# Most nix scripts write files, or shell scripts that can be +# executed later on: +# +# Writing JSON configuration files: +# ```nix +# config-file = pkgs.writeText "config.json" (builtins.toJSON { +# database = { host = "localhost"; port = 5432; }; +# features = [ "auth" "logging" "metrics" ]; +# }); +# ``` +# +# Writing YAML configuration files: +# ```nix +# yamlFormat = pkgs.formats.yaml { }; +# yaml-config = yamlFormat.generate "config.yaml" { +# database = { +# host = dbHost; +# port = dbPort; +# }; +# features = [ "auth" "logging" "metrics" ]; +# }; +# ``` +# +# Writing shell scripts with string interpolation: +# ```nix +# my-script = pkgs.writeScript "deploy.sh" '' +# #!/bin/bash +# +# # Nix string interpolation with ${} +# APP_NAME="${pname}" +# VERSION="${version}" +# BUILD_DIR="${placeholder "out"}" +# +# echo "Deploying $APP_NAME version $VERSION" +# echo "Build output: $BUILD_DIR" +# +# # Multi-line strings preserve formatting +# cat << EOF > deployment.yaml +# apiVersion: apps/v1 +# kind: Deployment +# metadata: +# name: ${pname} +# spec: +# replicas: 3 +# EOF +# ''; +# ``` +# +# Writing files in build phases: +# ```nix +# my-package = pkgs.stdenv.mkDerivation { +# # ... +# buildPhase = '' +# # Generate a config file during build +# cat > config.ini << EOF +# [database] +# host=${dbHost} +# port=${toString dbPort} +# +# [app] +# name=${pname} +# debug=${if enableDebug then "true" else "false"} +# EOF +# +# # Build the application +# make build +# ''; +# installPhase = '' +# mkdir -p $out/bin $out/etc +# cp my-app $out/bin/ +# cp config.ini $out/etc/ +# ''; +# }; +# ``` +# +# Key string features: +# • '' multiline strings preserve indentation and newlines +# • ${...} interpolates Nix expressions into strings +# • ${placeholder "out"} references build outputs safely +# • builtins.toJSON converts Nix data to JSON strings +# • pkgs.formats.yaml.generate converts Nix attribute sets to YAML +# + From 6557a6125befeb203c9828d6656b8e262ef25724 Mon Sep 17 00:00:00 2001 From: Ajay Ganapathy Date: Tue, 16 Dec 2025 22:16:00 -0500 Subject: [PATCH 03/18] chore: set up dev shell chore: set up dev shell Signed-off-by: Ajay Ganapathy --- .config/.envrc | 1 + .config/.gitignore | 8 + .config/devShell.nix | 346 +++++++++++++++++++++++++++ .config/importFromLanguageFolder.nix | 33 +++ .config/language-nix/.envrc | 1 + .config/language-nix/.gitignore | 5 + .config/language-nix/devShell.nix | 77 ++++++ flake.nix | 24 +- 8 files changed, 494 insertions(+), 1 deletion(-) create mode 100644 .config/.envrc create mode 100644 .config/.gitignore create mode 100644 .config/devShell.nix create mode 100644 .config/importFromLanguageFolder.nix create mode 100644 .config/language-nix/.envrc create mode 100644 .config/language-nix/.gitignore create mode 100644 .config/language-nix/devShell.nix diff --git a/.config/.envrc b/.config/.envrc new file mode 100644 index 0000000..9de2cce --- /dev/null +++ b/.config/.envrc @@ -0,0 +1 @@ +use flake ../#nix diff --git a/.config/.gitignore b/.config/.gitignore new file mode 100644 index 0000000..4c3e54a --- /dev/null +++ b/.config/.gitignore @@ -0,0 +1,8 @@ +* +!language-nix +!.envrc +!.gitignore +!configVscode.nix +!importFromLanguageFolder.nix +!configZed.nix +!devShell.nix diff --git a/.config/devShell.nix b/.config/devShell.nix new file mode 100644 index 0000000..3bc0dc1 --- /dev/null +++ b/.config/devShell.nix @@ -0,0 +1,346 @@ +{ + pkgs ? import {}, + devShellConfigs ? (import ./importFromLanguageFolder.nix {inherit pkgs;}).importDevShell, +}: let + validateConfigAttrs = c: + if !builtins.hasAttr "name" c + then throw "invalid config, missing name: ${c}" + else if !builtins.hasAttr "packages" c + then throw "invalid config, missing packages: ${c}" + else if !builtins.hasAttr "shellHook" c + then throw "invalid config, missing shellHook: ${c}" + else true; + validatePackage = p: + if !builtins.isAttrs p + then throw "invalid package, not an attrset: ${p}" + else if !builtins.hasAttr "name" p + then throw "invalid package, missing name: ${p}" + else if !builtins.hasAttr "meta" p + then throw "invalid package ${p.name}, missing meta: ${p}" + else if !builtins.hasAttr "description" p.meta + then throw "invalid package ${p.name}, missing description: ${p}" + else if !builtins.pathExists "${p}/bin" + then throw "invalid package ${p.name}, missing /bin dir: ${p}" + else if builtins.readDir "${p}/bin" == {} + then throw "invalid package ${p.name}, empty /bin dir: ${p}" + else true; + validateUniquePackages = packages: let + packageNames = map (package: package.name) packages; + grouped = pkgs.lib.groupBy (name: name) packageNames; + duplicates = builtins.attrNames (pkgs.lib.filterAttrs (name: names: builtins.length names > 1) grouped); + in + duplicates == [] || throw "Duplicate package names found: ${toString duplicates}"; + validDevShellConfigs = map (c: + if + builtins.isAttrs c + && validateConfigAttrs c + && (builtins.all (x: x) (map (package: validatePackage package) c.packages)) + && validateUniquePackages c.packages + then c + else builtins.throw "invalid devShellConfig ${c}") + devShellConfigs; + wrappedPackages = devShellConfig: + pkgs.lib.fix ( + let + packages = builtins.listToAttrs ( + builtins.map (package: { + name = package.name; + value = package; + }) + devShellConfig.packages + ); + name = devShellConfig.name; + in ( + self: + packages + // ( + if builtins.hasAttr "project-lint" packages + then { + project-lint = pkgs.writeShellApplication { + name = "project-lint"; + meta = packages.project-lint.meta; + text = '' + ${packages.project-lint}/bin/project-lint + ''; + }; + } + else throw "devShellConfig ${name} missing project-lint" + ) + // ( + if builtins.hasAttr "project-build" packages + then { + project-build = pkgs.writeShellApplication { + name = "project-build"; + meta = packages.project-build.meta; + text = '' + ${packages.project-build}/bin/project-build + ''; + }; + } + else throw "devShellConfig ${name} missing project-build" + ) + // ( + if builtins.hasAttr "project-test" packages + then { + project-test = pkgs.writeShellApplication { + name = "project-test"; + meta = packages.project-test.meta; + # todo pass a list of files that changed in current commit + text = '' + ${packages.project-test}/bin/project-test + ''; + }; + } + else throw "devShellConfig ${name} missing project-test" + ) + ) + ); + listBins = package: builtins.map (p: p.name) (builtins.filter (dirent: dirent.value != "directory") (pkgs.lib.attrsToList (builtins.readDir "${package}/bin"))); + hasBins = package: pkgs.lib.pathExists "${package}/bin" && (builtins.length (listBins package) > 0); + filterPackagesWithBins = packages: builtins.filter (package: hasBins package) packages; + writeCommandDescription = package: + ( + if builtins.length (listBins package) == 1 + then '' + `${package.name}` + '' + else '' + ${builtins.concatStringsSep ", " (builtins.map (bin: "`${bin}`") (listBins package))} + '' + ) + + '' + > ${package.meta.description} + + ''; + writeCommandDescriptions = packages: + map (package: writeCommandDescription package) (filterPackagesWithBins packages); + makeDevShell = devShellConfig: pkgs: + pkgs.mkShell { + # make the packages available in the dev shell + packages = with pkgs; + [coreutils glow] + ++ builtins.attrValues ( + # read the packages in devShellConfig, get the lint, lintSemVer, build, runTest, publishDryRun and publish packages and wrap them before re-emitting them into the list of packages + wrappedPackages devShellConfig + ); + shellHook = let + commandDescriptions = writeCommandDescriptions (builtins.attrValues (wrappedPackages devShellConfig)); + in + # run any hooks specific to this dev shell + devShellConfig.shellHook + # ... and then print the list of available commands with their descriptions + + '' + ${pkgs.glow}/bin/glow <<-'EOF' >&2 + ${builtins.concatStringsSep "\n" commandDescriptions} + EOF + ''; + }; + devShells = + (builtins.listToAttrs ( + map (config: { + name = config.name; + value = makeDevShell config pkgs; + }) + validDevShellConfigs + )) + // { + default = let + p = []; + commandDescriptions = writeCommandDescriptions p; + in + pkgs.mkShell { + packages = [pkgs.glow pkgs.git] ++ p; + shellHook = '' + if [ ! -d .git ] + then + echo "no .git/ found, are you in the root of the repository?" >&2 + exit 1 + fi + + ${pkgs.glow}/bin/glow <<-'EOF' >&2 + ${builtins.concatStringsSep "\n" commandDescriptions} + EOF + ''; + }; + }; +in { + inherit + # language-specific dev shell configs and function to turn dev shell configs into dev shells + # exported to make it possible for child flakes to inherit from dev shell configs and make dev shells + validDevShellConfigs + makeDevShell + # the language-specific dev shells and root dev shell, used in the root flake + devShells + ; +} +# +# LANGUAGE-SPECIFIC DEVELOPMENT SHELLS +# +# This nix expression builds specialized development shells, one for +# each language-* folder +# +# each project in this monorepo uses exactly ONE dev shell +# i.e. +# +# nix project ......... nix dev shell +# +# go project ......... go dev shell +# +# typescript typescript +# project ......... dev shell +# +# each dev shell sets up the dev tools you need to +# work in its respective language +# +# projects/ +# |-- flake.nix <---. +# : | +# | imports +# '-- .config/ | +# | | +# |- devShell.nix <-- imports --, +# | | +# |- importFromLanguageFolder.nix <----------, +# : | +# : imports +# : | +# | -, | +# |-- language-nix/ | | +# | | | | +# | : | | +# | | | | +# | '-- devShell.nix | | +# | | | +# |-- language-go/ | | +# | | +---------' +# | : | +# | | | +# | '-- devShell.nix | +# | | +# '-- language-typescript/ | +# | | +# '-- devShell.nix | +# -' +# +# all languages are added to the root flake.nix's development +# shells. +# +# To use a development shell, you can run nix develop ./# +# e.g. `nix develop ./#nix` to load the `language-nix` dev shell or +# `nix develop ./#go` to load the `language-go` dev shell +# +# WHY DEV SHELLS +# +# Project tooling is the catch-22 of learning a new language. You +# need a DEEP understanding of a language in order to set up its +# tooling correctly, but you CAN'T gain a deep understanding of the +# language without first trying it out! Nix dev shells install +# project tooling for you, so you can get straight to learning. +# Every time you use a nix dev shell, you skip over the 3+ weeks +# of work you would have needed to spend to get to "hello world" +# +# Dev shells scale across the projects in the monorepo. All projects +# of a language use the SAME EXACT VERSION and CONFIGURATION of +# the project tools. This eliminates version-mismatch bugs from +# the codebase. +# +# HOW TO SET UP A LANGUAGE-SPECIFIC DEV SHELL +# +# Each language-specific folder contains a devShell.nix. This +# nix file must contain +# the following nix expression +# +# { pkgs ? import {}}: let +# devShellConfig = { +# packages = [ +# (pkgs.writeShellApplication { +# name = "project-lint"; +# meta = { +# description = "..." # description of what gets linted +# }; +# runtimeInputs = with pkgs; [ +# ... # packages used to lint project files +# ]; +# text = '' +# ... # command used to lint project files +# ''; +# }) +# +# (pkgs.writeShellApplication { +# name = "project-build"; +# meta = { +# description = "..." # description of what gets built +# }; +# runtimeInputs = with pkgs; [ +# ... # packages used to build project files +# ]; +# text = '' +# ... # command used to build project files +# # command used to print project files +# # to stdout, separated by null bytes +# ''; +# }) +# +# (pkgs.writeShellApplication { +# name = "project-test"; +# meta = { +# description = "..." # description of what gets tested +# }; +# runtimeInputs = with pkgs; [ +# ... # packages used to test project files +# ]; +# text = '' +# ... # command used to test project files +# ''; +# }) +# +# (pkgs.writeShellApplication { +# name = "project-*"; # any other project-specific script +# meta = { # +# description = "..." # MAKE SURE YOU PREPEND "project-" +# }; # TO THE NAME OF ANY BUILD SCRIPT +# runtimeInputs = with pkgs; [ # this makes it easy to tab-complete +# ... # all project-* specific commands +# ]; +# text = '' +# ... +# ''; +# }) +# ... # any other packages that need to be +# # available in the dev environment +# ]; +# shellHook = '' # any commands you want to run on +# ... # entry into the project environment +# ''; # (e.g. dependency installation +# # or cleanup commands) +# } +# in +# devShellConfig +# +# this devShell.nix composes the contents of the language-specific dev-shell.nix: +# ________________________ ________________________ +# / devShell.nix | / language-* | +# / | / devShell.nix | +# | ----------------------- | | ----------------------- | +# | packages | | packages | +# | project-lint <------ wrapped by ------ project-lint | +# | | | | +# | project-build <---- wrapped by ------ project-build | +# | | | | +# | project-test <----- wrapped by ------ project-test | +# | | | | +# | ... <---- directly imported into ----- ... | +# | | | | +# | shellHook <----- runs before -------- shellHook | +# |_________________________| |_________________________| +# +# +# Every dev shell provides the following commands: +# +# project-lint +# project-build +# project-test +# +# While the command names do not vary across dev shells, their implementations do. +# These commands provide git hooks and CI a common interface for running project-specific tools. + diff --git a/.config/importFromLanguageFolder.nix b/.config/importFromLanguageFolder.nix new file mode 100644 index 0000000..12a80c7 --- /dev/null +++ b/.config/importFromLanguageFolder.nix @@ -0,0 +1,33 @@ +# +# import configVscode.nix, configZed.nix, stubProject.nix, devShell.nix from language-* subfolders +# +{pkgs ? import {}}: let + # Get all language directories + configContents = builtins.readDir ./.; + languageDirs = + builtins.filter + (name: pkgs.lib.hasPrefix "language-" name) + (pkgs.lib.attrNames (pkgs.lib.filterAttrs (name: type: type == "directory") configContents)); + + getExistingFiles = configFile: + builtins.filter (path: builtins.pathExists path.file) + (map (dir: { + language = pkgs.lib.removePrefix "language-" dir; + file = ./. + "/${dir}/${configFile}"; + }) + languageDirs); + + # Get all existing paths for each config type + importConfigVscode = map (f: import f.file {inherit pkgs;}) (getExistingFiles "configVscode.nix"); + importConfigZed = map (f: import f.file {inherit pkgs;}) (getExistingFiles "configZed.nix"); + importStubProject = map ( + f: + (import f.file {inherit pkgs;}) // {devShellName = f.language;} + ) (getExistingFiles "stubProject.nix"); + importDevShell = map ( + f: + (import f.file {inherit pkgs;}) // {name = f.language;} + ) (getExistingFiles "devShell.nix"); +in { + inherit importConfigVscode importConfigZed importStubProject importDevShell; +} diff --git a/.config/language-nix/.envrc b/.config/language-nix/.envrc new file mode 100644 index 0000000..fb193de --- /dev/null +++ b/.config/language-nix/.envrc @@ -0,0 +1 @@ +use flake ../../#nix diff --git a/.config/language-nix/.gitignore b/.config/language-nix/.gitignore new file mode 100644 index 0000000..426c05f --- /dev/null +++ b/.config/language-nix/.gitignore @@ -0,0 +1,5 @@ +* +!.gitignore +!configZed.nix +!configVscode.nix +!devShell.nix diff --git a/.config/language-nix/devShell.nix b/.config/language-nix/devShell.nix new file mode 100644 index 0000000..effa064 --- /dev/null +++ b/.config/language-nix/devShell.nix @@ -0,0 +1,77 @@ +{pkgs ? import {}}: let + devShellConfig = { + packages = [ + # make nix package manager available in the dev env + pkgs.nix + # receives a newline-separated list of files to lint + (pkgs.writeShellApplication + { + name = "project-lint"; + meta = { + description = "lint all .nix files"; + }; + runtimeInputs = with pkgs; [ + alejandra + fd + ]; + text = '' + # Find all .nix files and store in bash array + mapfile -d ''' -t nixfiles < <(fd --type f '\.nix$' -0) + + if [ ''${#nixfiles[@]} -gt 0 ]; then + alejandra -c "''${nixfiles[@]}" >&2 + fi + ''; + }) + (pkgs.writeShellApplication + { + name = "project-build"; + meta = { + description = "build the default package in the project's flake.nix"; + }; + runtimeInputs = with pkgs; [ + coreutils + fd + nix + ]; + text = '' + # Run nix build and capture output + if [ ! -f "flake.nix" ]; then + echo "no flake.nix in ''${PWD}. Nothing to build" >&2 + exit 0 + fi + if ! nix build; then + echo "error" >&2 + exit 1 + fi + + # nix build will always output result* symlinks e.g. result/, result-dev/, result-docs/ ... + # print absolute path to each, split paths by null bytes + fd --max-depth 1 --type l "result*" -0 --absolute-path + ''; + }) + # run all checks in the current project's flake + (pkgs.writeShellApplication + { + name = "project-test"; + meta = { + description = "run all checks in a project's flake.nix"; + }; + runtimeInputs = with pkgs; [ + nix + ]; + text = '' + if [ ! -f "flake.nix" ]; then + echo "no flake.nix ''${PWD}, nothing to flake check" >&2 + exit 0 + fi + + echo "🧪 Running nix flake check..." >&2 + nix flake check + ''; + }) + ]; + shellHook = ''''; + }; +in + devShellConfig diff --git a/flake.nix b/flake.nix index bdbfa45..b877f10 100644 --- a/flake.nix +++ b/flake.nix @@ -48,9 +48,31 @@ }; # Flake outputs that other flakes can use - outputs = {flake-schemas, ...}: { + outputs = { + flake-schemas, + nixpkgs, + ... + }: let + # Helpers for producing system-specific outputs + supportedSystems = ["x86_64-linux" "aarch64-darwin" "x86_64-darwin" "aarch64-linux"]; + forEachSupportedSystem = f: + nixpkgs.lib.genAttrs supportedSystems (system: + f { + pkgs = import nixpkgs {inherit system;}; + }); + in { # Schemas tell Nix about the structure of your flake's outputs schemas = flake-schemas.schemas; + makeDevShell = forEachSupportedSystem ({pkgs}: (import ./.config/devShell.nix {inherit pkgs;}).makeDevShell); + validDevShellConfigs = forEachSupportedSystem ({pkgs}: (import ./.config/devShell.nix {inherit pkgs;}).validDevShellConfigs); + + # Development environments + devShells = forEachSupportedSystem ( + {pkgs}: let + languageDevShells = (import ./.config/devShell.nix {inherit pkgs;}).devShells; + in + languageDevShells + ); }; } # New to nix? Confused by the syntax of this flake? From 30e58ae0017d618237493ba9263980375cfa4fde Mon Sep 17 00:00:00 2001 From: Ajay Ganapathy Date: Sun, 16 Nov 2025 15:03:14 -0500 Subject: [PATCH 04/18] chore: set up vscode configuration Signed-off-by: Ajay Ganapathy --- .config/configVscode.nix | 161 ++++++++++++++++++++++++++ .config/devShell.nix | 6 +- .config/language-nix/configVscode.nix | 22 ++++ flake.nix | 8 ++ 4 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 .config/configVscode.nix create mode 100644 .config/language-nix/configVscode.nix diff --git a/.config/configVscode.nix b/.config/configVscode.nix new file mode 100644 index 0000000..a0af102 --- /dev/null +++ b/.config/configVscode.nix @@ -0,0 +1,161 @@ +{ + pkgs ? import {}, + vscodeConfigs ? (import ./importFromLanguageFolder.nix {inherit pkgs;}).importConfigVscode, +}: let + validVscodeConfigs = builtins.map (vsc: + if + (builtins.isAttrs vsc) + && (builtins.hasAttr "vscodeSettings" vsc) + && (builtins.hasAttr "vscodeExtensions" vsc) + && (builtins.hasAttr "vscodeLaunch" vsc) + && (builtins.hasAttr "vscodeTasks" vsc) + then vsc + else builtins.throw "Invalid vscode configuration ${vsc}") + vscodeConfigs; + jsonFormatter = pkgs.formats.json {}; + vscodeSettings = jsonFormatter.generate "settings.json" ( + pkgs.lib.lists.fold (set: acc: pkgs.lib.attrsets.recursiveUpdate acc set) {} (builtins.map (vsc: vsc.vscodeSettings) validVscodeConfigs) + ); + vscodeExtensions = jsonFormatter.generate "extensions.json" ( + pkgs.lib.lists.fold (set: acc: pkgs.lib.attrsets.recursiveUpdate acc set) {} (builtins.map (vsc: vsc.vscodeExtensions) validVscodeConfigs) + ); + vscodeLaunch = jsonFormatter.generate "launch.json" ( + pkgs.lib.lists.fold (set: acc: pkgs.lib.attrsets.recursiveUpdate acc set) {} (builtins.map (vsc: vsc.vscodeLaunch) validVscodeConfigs) + ); + vscodeTasks = jsonFormatter.generate "tasks.json" ( + pkgs.lib.lists.fold (set: acc: pkgs.lib.attrsets.recursiveUpdate acc set) {} (builtins.map (vsc: vsc.vscodeTasks) validVscodeConfigs) + ); + vscodeConfiguration = pkgs.stdenv.mkDerivation { + name = "vscodeConfiguration"; + src = null; + phases = [ + "buildPhase" + ]; + buildPhase = '' + mkdir -p $out + cd $out + + ln -s ${vscodeSettings} settings.json + ln -s ${vscodeExtensions} extensions.json + ln -s ${vscodeLaunch} launch.json + ln -s ${vscodeTasks} tasks.json + ''; + }; + project-install-vscode-configuration = pkgs.writeShellApplication { + name = "project-install-vscode-configuration"; + meta = { + description = "install .vscode/ configuration folder, if .vscode/ is not already present. Automatically run when this shell is opened."; + }; + runtimeInputs = [pkgs.coreutils]; + text = '' + if [ ! -d "./.git" ]; then + echo "please run this script from the root of the monorepo" >&2 && exit 1 + fi + + VSCODE_DIR=$(readlink -f "./.vscode") + + if [ ! -e "./.vscode" ]; then + ln -s ${vscodeConfiguration} "./.vscode" + echo "✅ linked ${vscodeConfiguration} to ./.vscode" >&2 + exit 0 + fi + + if [ "$VSCODE_DIR" = ${vscodeConfiguration} ]; then + echo "✅ vscode configuration already linked" >&2 + exit 0 + fi + + if [ "$(dirname "$VSCODE_DIR")" = "$(dirname ${vscodeConfiguration})" ]; then + unlink "./.vscode" + ln -s ${vscodeConfiguration} "./.vscode" + echo "✅ vscode configuration updated" >&2 + exit 0 + fi + + LINK_INSTEAD=$(basename ${vscodeConfiguration}) + + ln -s ${vscodeConfiguration} "./$LINK_INSTEAD" + + echo "❌ cannot link vscode configuration because ./.vscode directory already exists." >&2 + echo " Linking ${vscodeConfiguration} to ./$LINK_INSTEAD instead" >&2 + echo " Please merge this configuration with your ./.vscode folder" >&2 + ''; + }; +in + project-install-vscode-configuration +# HOW TO SET UP A LANGUAGE-SPECIFIC VSCODE CONFIGURATION +# +# Each language-specific folder contains a configVscode.nix. This +# nix file must contain the following nix expression: +# +# { pkgs ? import {} }: { +# vscodeSettings = { +# "some.setting" = true; # settings merged into settings.json +# "editor.formatOnSave" = true; # use exact VSCode setting keys +# "some.path" = "${pkgs.tool}/bin/t"; # nix store paths are supported +# }; +# vscodeExtensions = { +# "recommendations" = [ +# "publisher.extension-id" # extensions merged into extensions.json +# ]; +# }; +# vscodeLaunch = { +# "configurations" = [ # launch configs merged into launch.json +# { +# "type" = "node"; +# "request" = "launch"; +# "name" = "Debug Program"; +# "program" = "\${workspaceFolder}/src/main.js"; +# } +# ]; +# }; +# vscodeTasks = { +# "tasks" = [ # tasks merged into tasks.json +# { +# "label" = "build"; +# "type" = "shell"; +# "command" = "project-build"; +# } +# ]; +# }; +# } +# +# this configVscode.nix merges the contents of all language-specific configVscode.nix: +# +# ________________________ ________________________ +# / .vscode/ | / language-* | +# / settings.json | / configVscode.nix | +# | ----------------------- | | ----------------------- | +# | { | | vscodeSettings = { | +# | "nix.enable": true,<---- merged ------ "nix.enable" = true; | +# | "go.enable": true <---- from all ---- ... | +# | ... | langs | }; | +# | } | | | +# |_________________________| | vscodeExtensions = { | +# | "recommendations" = [ | +# ________________________ | "ext.id" | +# / .vscode/ | | ]; | | +# / extensions.json | | }; | | +# | ----------------------- | | | | +# | { | | vscodeLaunch = { ... }; | +# | "recommendations": [ | | | | +# | "ext.id" <--------- | -- merged -----------------' | +# | ] | | vscodeTasks = { ... }; | +# | } | |_________________________| +# |_________________________| +# +# The merge strategy uses Nix's // operator with recursiveUpdate, +# so later language configs can override earlier ones if keys conflict. +# Each attrset corresponds to a VSCode configuration file: +# +# vscodeSettings --> .vscode/settings.json +# vscodeExtensions --> .vscode/extensions.json +# vscodeLaunch --> .vscode/launch.json +# vscodeTasks --> .vscode/tasks.json +# +# See VSCode documentation: +# • settings.json: https://code.visualstudio.com/docs/getstarted/settings +# • extensions.json: https://code.visualstudio.com/docs/editor/extension-marketplace#_workspace-recommended-extensions +# • launch.json: https://code.visualstudio.com/docs/editor/debugging#_launch-configurations +# • tasks.json: https://code.visualstudio.com/docs/editor/tasks + diff --git a/.config/devShell.nix b/.config/devShell.nix index 3bc0dc1..82450f9 100644 --- a/.config/devShell.nix +++ b/.config/devShell.nix @@ -145,7 +145,9 @@ )) // { default = let - p = []; + p = [ + (import ./configVscode.nix {inherit pkgs;}) + ]; commandDescriptions = writeCommandDescriptions p; in pkgs.mkShell { @@ -157,6 +159,8 @@ exit 1 fi + project-install-vscode-configuration + ${pkgs.glow}/bin/glow <<-'EOF' >&2 ${builtins.concatStringsSep "\n" commandDescriptions} EOF diff --git a/.config/language-nix/configVscode.nix b/.config/language-nix/configVscode.nix new file mode 100644 index 0000000..c3fb329 --- /dev/null +++ b/.config/language-nix/configVscode.nix @@ -0,0 +1,22 @@ +{pkgs ? import {}}: { + vscodeSettings = { + "nix.enableLanguageServer" = true; + "nix.serverPath" = "${pkgs.nixd}/bin/nixd"; + "nix.serverSettings" = { + "nixd" = { + "formatting" = { + "command" = [ + "${pkgs.alejandra}/bin/alejandra" + ]; + }; + }; + }; + }; + vscodeExtensions = { + "recommendations" = [ + "jnoortheen.nix-ide" + ]; + }; + vscodeLaunch = {}; + vscodeTasks = {}; +} diff --git a/flake.nix b/flake.nix index b877f10..986a1fe 100644 --- a/flake.nix +++ b/flake.nix @@ -288,6 +288,9 @@ # ^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^ # Add any tools your build needs # +# phases = [ buildPhase ] # see .config/commitlint.nix for +# # a detailed explanation of phases +# # # Build commands go in buildPhase # buildPhase = '' # npm install @@ -455,6 +458,8 @@ # If you want to build the C project, without building # the Go and JS project, you can `nix build .#example-c-project` # +# To see this in action, go to .config/commitlint.nix +# # DEBUGGING NIX BUILD # # use `nix build --log-format raw .#name-of-pkg` to print @@ -1287,6 +1292,9 @@ # }; # ``` # +# see .config/commitlint-config.nix for an example +# of pkgs.formats.yaml +# # Writing shell scripts with string interpolation: # ```nix # my-script = pkgs.writeScript "deploy.sh" '' From fdd70c3aaed249b34ce9a1cd65e1d7d260a227a4 Mon Sep 17 00:00:00 2001 From: Ajay Ganapathy Date: Sun, 16 Nov 2025 15:03:35 -0500 Subject: [PATCH 05/18] chore: set up zed configuration Signed-off-by: Ajay Ganapathy --- .config/configZed.nix | 182 +++++++++++++++++++++++++++++ .config/devShell.nix | 2 + .config/language-nix/configZed.nix | 34 ++++++ 3 files changed, 218 insertions(+) create mode 100644 .config/configZed.nix create mode 100644 .config/language-nix/configZed.nix diff --git a/.config/configZed.nix b/.config/configZed.nix new file mode 100644 index 0000000..e250ed1 --- /dev/null +++ b/.config/configZed.nix @@ -0,0 +1,182 @@ +{ + pkgs ? import {}, + zedConfigs ? (import ./importFromLanguageFolder.nix {inherit pkgs;}).importConfigZed, +}: let + validZedConfigs = builtins.map (zc: + if (builtins.isAttrs zc) && (builtins.hasAttr "zedSettings" zc) && (builtins.hasAttr "zedDebug" zc) + then zc + else builtins.throw "invalid zedConfig ${builtins.toJSON zc}") + zedConfigs; + jsonFormatter = pkgs.formats.json {}; + zedSettings = jsonFormatter.generate "settings.json" ( + pkgs.lib.lists.fold (set: acc: pkgs.lib.attrsets.recursiveUpdate acc set) {} + ( + (builtins.map (zc: zc.zedSettings) validZedConfigs) + ++ [ + { + # SHELL HOOK MODE (`"load_direnv": "shell_hook"`): + # + # Zed scoops the env vars from the $PATH of the builtin terminal + # direnv automatically loads env vars into the builtin terminal + # therefore, Zed receives the env vars from direnv + # + # ``` + # ____________ _____________ ____________ + # | .envrc | | shell with | | Zed Editor | + # | file in | | direnv hook | | | + # | project +----------->| activated +----------->| | + # | directory | reads | | inherits | | + # |____________| |_____________| env |____________| + # | ^ + # | | + # modifies shell env uses env vars to: + # | | + # v | + # ____________ _____________ ____+_______ + # | $PATH | | environment | | Language | + # | $RUST_SRC |<-----------+ variables | | Servers & | + # | $GOPATH | exports | set by | | Extensions | + # | etc... | | direnv | |____________| + # |____________| |_____________| + # ``` + # + # When Zed uses the "shell_hook" mode: + # 1. Zed launches a shell process in your project directory + # 2. The direnv hook in that shell activates and processes your .envrc + # 3. Direnv modifies the shell's environment variables + # 4. Zed inherits these variables from the shell + # 5. Language servers and extensions use these variables for configuration + # + # This approach ensures that Zed sees the same environment that your + # terminal would see when working in that directory. + # + "load_direnv" = "shell_hook"; + } + ] + ) + ); + zedDebug = jsonFormatter.generate "debug.json" (builtins.filter (item: item != {}) (builtins.map (zc: zc.zedDebug) validZedConfigs)); + zedConfiguration = pkgs.stdenv.mkDerivation { + name = "zedConfiguration"; + src = null; + phases = [ + "buildPhase" + ]; + buildPhase = '' + mkdir -p $out + cd $out + + ln -s ${zedSettings} settings.json + ln -s ${zedDebug} debug.json + ''; + }; + project-install-zed-configuration = pkgs.writeShellApplication { + name = "project-install-zed-configuration"; + meta = { + description = "install .zed/ configuration folder, if .zed/ is not already present. Automatically run when this shell is opened"; + }; + runtimeInputs = [pkgs.coreutils]; + text = '' + if [ ! -d "./.git" ]; then + echo "please run this script from the root of the monorepo" >&2 && exit 1 + fi + + ZED_DIR=$(readlink -f "./.zed") + + if [ ! -e "./.zed" ]; then + ln -s ${zedConfiguration} "./.zed" + echo "✅ linked ${zedConfiguration} to ./.zed" >&2 + exit 0 + fi + + if [ "$ZED_DIR" = ${zedConfiguration} ]; then + echo "✅ zed configuration already linked" >&2 + exit 0 + fi + + if [ "$(dirname "$ZED_DIR")" = "$(dirname ${zedConfiguration})" ]; then + unlink "./.zed" + ln -s ${zedConfiguration} "./.zed" + echo "✅ zed configuration updated" >&2 + exit 0 + fi + + LINK_INSTEAD=$(basename ${zedConfiguration}) + + ln -s ${zedConfiguration} "./$LINK_INSTEAD" + + echo "❌ cannot link zed configuration because ./.zed directory already exists." >&2 + echo " Linking ${zedConfiguration} to ./$LINK_INSTEAD instead" >&2 + echo " Please merge this configuration with your ./.zed folder" >&2 + ''; + }; +in + project-install-zed-configuration +# HOW TO SET UP A LANGUAGE-SPECIFIC ZED CONFIGURATION +# +# Each language-specific folder contains a configZed.nix. This +# nix file must contain the following nix expression: +# +# { pkgs ? import {} }: { +# zedSettings = { +# "auto_install_extensions" = { # auto-install language extensions +# "LanguageName" = true; +# }; +# "languages" = { +# "LanguageName" = { +# "language_servers" = [ # configure language servers +# "server-name" +# ]; +# "formatter" = { # configure formatter +# "external" = { +# "command" = "${pkgs.tool}/bin/fmt"; +# "arguments" = ["--flag"]; +# }; +# }; +# }; +# }; +# "lsp" = { +# "server-name" = { # LSP binary configuration +# "binary" = { +# "path" = "${pkgs.lsp}/bin/lsp"; +# }; +# }; +# }; +# }; +# zedDebug = { # debug adapter configurations +# "adapter" = "adapter-name"; # merged into debug.json +# "program" = "${workspaceFolder}/bin"; +# }; # use {} if no debugger for language +# } +# +# this configZed.nix merges the contents of all language-specific configZed.nix: +# +# ________________________ ________________________ +# / .zed/ | / language-* | +# / settings.json | / configZed.nix | +# | ----------------------- | | ----------------------- | +# | { | | zedSettings = { | +# | "languages": { <------ merged ------ "languages" = { | +# | "Nix": {...}, <---- from all ---- "Nix" = {...}; | +# | "Go": {...} | langs | }; | +# | } | | }; | +# | } | | | +# |_________________________| | zedDebug = { | +# | ... | | +# ________________________ | }; | | +# / .zed/ | | | | +# / debug.json | |_______________|_________| +# | ----------------------- | | +# | [ | | +# | {...} <-------------- | -------- merged -----------' +# | ] | +# |_________________________| +# +# The merge strategy uses Nix's // operator with recursiveUpdate, +# so later language configs can override earlier ones if keys conflict. +# Each attrset corresponds to a Zed configuration file: +# +# zedSettings --> .zed/settings.json +# zedDebug --> .zed/debug.json +# + diff --git a/.config/devShell.nix b/.config/devShell.nix index 82450f9..6cb9391 100644 --- a/.config/devShell.nix +++ b/.config/devShell.nix @@ -147,6 +147,7 @@ default = let p = [ (import ./configVscode.nix {inherit pkgs;}) + (import ./configZed.nix {inherit pkgs;}) ]; commandDescriptions = writeCommandDescriptions p; in @@ -160,6 +161,7 @@ fi project-install-vscode-configuration + project-install-zed-configuration ${pkgs.glow}/bin/glow <<-'EOF' >&2 ${builtins.concatStringsSep "\n" commandDescriptions} diff --git a/.config/language-nix/configZed.nix b/.config/language-nix/configZed.nix new file mode 100644 index 0000000..00d68fa --- /dev/null +++ b/.config/language-nix/configZed.nix @@ -0,0 +1,34 @@ +{pkgs ? import {}}: { + zedSettings = { + "auto_install_extensions" = { + "Nix" = true; + }; + "languages" = { + "Nix" = { + "language_servers" = [ + "nixd" + "!nil" + ]; + "formatter" = { + "external" = { + "command" = "${pkgs.alejandra}/bin/alejandra"; + "arguments" = [ + "--quiet" + "--" + ]; + }; + }; + }; + }; + "lsp" = { + "nixd" = { + # see: https://zed.dev/docs/configuring-languages + "binary" = { + "ignore_system_version" = false; + "path" = "${pkgs.nixd}/bin/nixd"; + }; + }; + }; + }; + zedDebug = {}; # there are no debuggers for nix +} From b9947b33052b2e00e28bb653c34e5960c4d2b98d Mon Sep 17 00:00:00 2001 From: Ajay Ganapathy Date: Wed, 17 Dec 2025 22:47:55 -0500 Subject: [PATCH 06/18] chore: make project-stub-nix command create a blank folder with a - README.md - CONTRIBUTE.md - .envrc - .gitignore - flake.nix Signed-off-by: Ajay Ganapathy --- .config/.gitignore | 2 + .config/devShell.nix | 41 ++- .config/language-nix/.gitignore | 1 + .config/language-nix/stubProject.nix | 171 +++++++++++++ .config/stubProject.nix | 368 +++++++++++++++++++++++++++ 5 files changed, 578 insertions(+), 5 deletions(-) create mode 100644 .config/language-nix/stubProject.nix create mode 100644 .config/stubProject.nix diff --git a/.config/.gitignore b/.config/.gitignore index 4c3e54a..ea16999 100644 --- a/.config/.gitignore +++ b/.config/.gitignore @@ -6,3 +6,5 @@ !importFromLanguageFolder.nix !configZed.nix !devShell.nix +!sanitizeProjectName.nix +!stubProject.nix diff --git a/.config/devShell.nix b/.config/devShell.nix index 6cb9391..2cda871 100644 --- a/.config/devShell.nix +++ b/.config/devShell.nix @@ -145,10 +145,12 @@ )) // { default = let - p = [ - (import ./configVscode.nix {inherit pkgs;}) - (import ./configZed.nix {inherit pkgs;}) - ]; + p = + [ + (import ./configVscode.nix {inherit pkgs;}) + (import ./configZed.nix {inherit pkgs;}) + ] + ++ (import ./stubProject.nix {inherit pkgs;}); commandDescriptions = writeCommandDescriptions p; in pkgs.mkShell { @@ -235,6 +237,35 @@ in { # e.g. `nix develop ./#nix` to load the `language-nix` dev shell or # `nix develop ./#go` to load the `language-go` dev shell # +# When you stub a project, using one of the project-stub-* +# commands, the new project includes an .envrc file that +# loads the project's language's dev shell +# i.e. +# +# ,---._____ +# stub-project-nix ----> | project | _________ +# | +-------> / .envrc | +# '_________' | | +# | use | +# | ../#nix | +# |_________| +# +# ,---._____ +# stub-project-go ----> | project | _________ +# | +-------> / .envrc | +# '_________' | | +# | use | +# | ../#go | +# |_________| +# +# ,---._____ +# stub-project- ----> | project | _________ +# typescript | +-------> / .envrc | +# '_________' | | +# | use | +# | ../#typescript +# |_________| +# # WHY DEV SHELLS # # Project tooling is the catch-22 of learning a new language. You @@ -323,7 +354,7 @@ in { # in # devShellConfig # -# this devShell.nix composes the contents of the language-specific dev-shell.nix: +# this devShell.nix composes the contents of the language-specific devShell.nix: # ________________________ ________________________ # / devShell.nix | / language-* | # / | / devShell.nix | diff --git a/.config/language-nix/.gitignore b/.config/language-nix/.gitignore index 426c05f..1c30103 100644 --- a/.config/language-nix/.gitignore +++ b/.config/language-nix/.gitignore @@ -3,3 +3,4 @@ !configZed.nix !configVscode.nix !devShell.nix +!stubProject.nix diff --git a/.config/language-nix/stubProject.nix b/.config/language-nix/stubProject.nix new file mode 100644 index 0000000..9e4957e --- /dev/null +++ b/.config/language-nix/stubProject.nix @@ -0,0 +1,171 @@ +{pkgs ? import {}}: +pkgs.writeShellApplication { + name = "stubProject"; + runtimeInputs = [ + pkgs.coreutils + pkgs.git + ]; + text = '' + PROJECT_DIR="$1" + FLAKE_DIR="$2" + + if [ -z "$PROJECT_DIR" ]; then + echo "PROJECT_DIR not passed in as first argument" >&2 + exit 1 + fi + + if [ -z "$FLAKE_DIR" ]; then + echo "FLAKE_DIR not passed in as second argument" >&2 + exit 1 + fi + + PROJECT=''${PROJECT_DIR##*/} # Extract basename using parameter expansion + PROJECT=''${PROJECT//[^a-zA-Z0-9-]/_} # Replace invalid chars with underscore + + # make a flake.nix + cat <<-EOT > "$PROJECT_DIR/flake.nix" + # 0.0.0 + # DO NOT REMOVE THE PRECEDING LINE. + # To bump the semantic version and trigger + # an auto-release when this project is merged + # to main, increment the semantic version above + # + # WHEN AND HOW TO EDIT THIS FLAKE + # + # use this flake when you need to build a project that contains code + # from multiple languages, has custom build steps, or special test + # suites. + # + # This flake inherits the project-lint-semver, project-build and + # project-test commands from the nix dev shell. It includes a custom + # project-lint command, that you can configure to lint the different + # files in the project. + # + # Edit the packages.default to define what the project-build + # command builds. + # + # Edit the checks to define the tests that the project-test + # command runs. + # + # Edit the devShells -> default -> devShellConfig -> project-lint + # to define a custom lint script. + # + { + description = "build and test for $PROJECT"; + + inputs = { + parent-flake.url = "path:$FLAKE_DIR"; + }; + + outputs = { + self, + parent-flake, + }: let + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + forEachSupportedSystem = f: + parent-flake.inputs.nixpkgs.lib.genAttrs supportedSystems ( + system: + f { + pkgs = import parent-flake.inputs.nixpkgs {inherit system;}; + } + ); + in { + packages = forEachSupportedSystem ({pkgs}: { + default = pkgs.stdenv.mkDerivation { + name = "$PROJECT"; + src = "$FLAKE_DIR"; # Include entire repo as source + + nativeBuildInputs = with pkgs; [ + coreutils + # INCLUDE THE TOOLS YOU NEED TO BUILD YOUR PACKAGE HERE + ]; + + phases = [ + # "unpackPhase" + # "patchPhase" + # "configurePhase" + "buildPhase" + # "checkPhase" + # "installPhase" + # "fixupPhase" + # "installCheckPhase" + ]; + + buildPhase = ''' + + # make dirs included in package FHS + # see https://refspecs.linuxfoundation.org/FHS_3.0/fhs/index.html + # + mkdir -p "\$out"/{bin,lib,share,include} + + # copy executables to bin/ so they can be nix run + # copy libraries to lib/ + # copy docs, data to share/ + # copy headers to include/ + '''; + }; + }); + + checks = forEachSupportedSystem ({pkgs}: { + exampleTest = + pkgs.runCommand "exampleTest" { + nativeBuildInputs = [self.packages.\''${pkgs.system}.default]; + } ''' + # name of bin to run default package + echo "test passed" > "\$out" + '''; + }); + + devShells = forEachSupportedSystem ({pkgs}: { + default = let + devShellNix = pkgs.lib.head (pkgs.lib.filter (config: config.name == "nix") parent-flake.validDevShellConfigs.\''${pkgs.system}); + devShellConfig = { + packages = + (pkgs.lib.filter (pname: pname != "project-lint") devShellNix.packages) + ++ [ + (pkgs.writeShellApplication { + name = "project-lint"; + meta = { + description = "lint project files"; # list the file types the project-lint command should lint + runtimeInputs = with pkgs; [ + # include packages needed to lint project files + alejandra + ]; + }; + text = ''' + # add lint commands for non-nix files that you want to lint + + # lint all nix files in this directory + alejandra -c *.nix + '''; + }) + ]; + shellHook = devShellNix.shellHook; + }; + in + parent-flake.makeDevShell.\''${pkgs.system} devShellConfig pkgs; + }); + }; + } + EOT + + # stage flake.nix so Nix can see it + git -C "$PROJECT_DIR" add flake.nix + + # generate flake.lock for reproducibility + (cd "$PROJECT_DIR" && nix flake update) + + # overwrite the default .envrc to use the custom flake + cat <<-EOF > "$PROJECT_DIR/.envrc" + use flake + EOF + + # stage all project files + git -C "$PROJECT_DIR" add . + ''; +} diff --git a/.config/stubProject.nix b/.config/stubProject.nix new file mode 100644 index 0000000..d34bc54 --- /dev/null +++ b/.config/stubProject.nix @@ -0,0 +1,368 @@ +{ + pkgs ? import {}, + stubProjectConfigs ? (import ./importFromLanguageFolder.nix {inherit pkgs;}).importStubProject, +}: let + validStubProjectConfigs = let + configs = + builtins.map ( + projectConfig: + if builtins.isAttrs projectConfig && builtins.hasAttr "devShellName" projectConfig + then projectConfig + else builtins.throw "invalid projectConfig ${projectConfig}" + ) + stubProjectConfigs; + + # Check for duplicate devShellName values + names = builtins.map (config: config.devShellName) configs; + uniqueNames = pkgs.lib.unique names; + + # Throw error if there are duplicates + c = + if builtins.length names != builtins.length uniqueNames + then builtins.throw "Duplicate dev shell name values found: ${builtins.toJSON names}" + else configs; + in + c; + wrapStubProject = stubProject: pkgs: + pkgs.writeShellApplication { + name = "project-stub-" + stubProject.devShellName; # e.g. project-stub-nix project-stub-go + meta = { + description = "Stub a " + stubProject.devShellName + " project"; + }; + runtimeInputs = [ + pkgs.coreutils + pkgs.fd + stubProject + ]; + text = '' + if ! [ -e ".git" ]; then + echo "please run this script from the root of the monorepo" >&2 + exit 1 + fi + + echo "enter project name" >&2 + read -r name + + # validate project name + if [[ -z "$name" ]]; then + echo "Error: Project name cannot be empty" >&2 + exit 1 + elif [ -e "$name" ]; then + echo "$name already exists" >&2 + exit 1 + elif [[ ! "$name" =~ ^[a-z][a-z\/-]*[a-z]$ ]]; then + echo "Error: Project name '$name' must be at least two characters long. it must start and end with a lowercase alphabetical character. It can only contain alphabetical characters and -" >&2 + echo "Valid examples: my-project, hello-world, abc, a-b-c, my/project, my/project/a-b-c" >&2 + exit 1 + fi + + if ! mkdir -p "$name"; then + echo "❌ could not create directory $name" >&2 + exit 1 + fi + + # update the root dir gitignore + if [ -f .gitignore ]; then + echo "!$name">>.gitignore + echo "!$name/**">>.gitignore + fi + + # locate the flake.nix at the root of the monorepo + FLAKE_DIR="../" + + seekToRoot(){ + local parent + parent=$(dirname "$(realpath "$*")") + + if [ -d .git ]; then + return + else + FLAKE_DIR="$FLAKE_DIR../" + seekToRoot "$parent" + fi + } + + seekToRoot "$(pwd)" + + # add default readme and contribute + cat <<-EOF > "$name/README.md" + # $name + + + + + ## How to use $name: + + ### Installation: + + ### API Methods | Modules: + + ## How Project Name works: + + ## Roadmap: + + ## [Contribute](./CONTRIBUTE.md) + EOF + + cat <<-EOF > "$name/CONTRIBUTE.md" + # Contribute to $name: + + ## Develop: + + ### Repository Structure: + + | File or Folder | What does it do? | When should you modify it? | + | :------------- | :--------------- | :------------------------- | + | | | | + ## Test: + + ## Document: + + ## Deploy: + + + EOF + + cat <<-EOF > "$name/.envrc" + use flake "$FLAKE_DIR#${stubProject.devShellName}" + EOF + + # run the stubProject command, pass in the $name of the project and the $FLAKE_DIR + stubProject "$name" "$FLAKE_DIR" + + cat <<-'EOF' >"$name/.gitignore" + # ignore all + * + + # and then whitelist what you want to track + EOF + + # Whitelist files + while read -r filename; do + echo "!$filename" >> "$name/.gitignore" + done < <(fd --type f --max-depth 1 . "$name" --no-ignore --hidden --exec basename {} \;) + + # Whitelist directories and their contents + while read -r dirname; do + echo "!$dirname" >> "$name/.gitignore" + echo "!$dirname/**" >> "$name/.gitignore" + done < <(fd --type d --max-depth 1 . "$name" --no-ignore --hidden --exec basename {} \;) + + ''; + }; + stubProjects = + builtins.map (projectConfig: wrapStubProject projectConfig pkgs) + validStubProjectConfigs; +in + stubProjects +# +# LANGUAGE-SPECIFIC PROJECT TEMPLATES +# +# This nix expression builds a script that stubs a different +# project for each language-* folder +# +# each project in this monorepo is created by exactly ONE +# language-*/stubProject.nix +# i.e. +# +# nix project ......... language-nix/stubProject.nix +# +# go project ......... language-go/stubProject.nix +# +# typescript language-typescript/ +# project ......... stubProject.nix +# +# each stubProject script creates the files and folders +# you need to work in the project's respective language +# +# projects/ +# |-- flake.nix <------. +# : | +# | imports +# '-- .config/ | +# | | +# |- stubProject.nix <----------, +# | | +# | imports +# | | +# |- importFromLanguageFolder.nix <----------, +# : | +# : imports +# : | +# | -, | +# |-- language-nix/ | | +# | | | | +# | : | | +# | | | | +# | '-- stubProject.nix | | +# | | | +# |-- language-go/ | | +# | | +---------' +# | : | +# | | | +# | '-- stubProject.nix | +# | | +# '-- language-typescript/ | +# | | +# '-- stubProject.nix | +# -' +# +# the root flake provides one project-stub-* command +# for each language +# i.e. +# +# project-stub-nix --- creates ---> nix project +# +# project-stub-go --- creates ---> go project +# +# project-stub --- creates ---> typescript +# -typescript project +# +# each project-stub-* command updates the +# monorepo as follows: +# +# projects/ +# | +# |- .config/ +# | +# |- .github/ +# | +# |- .envrc +# | +# |- .gitignore <-- update .gitignore to +# | whitelist name-of-project +# |- flake.lock +# | +# |- flake.nix +# | +# |- LICENSE +# | +# : +# | +# '- name-of-project/ <-- create new project folder +# | +# |- .envrc <-- load dev shell from root flake.nix +# | +# |- README.md <-- template for a README +# | +# |- CONTRIBUTE.md <-- template for a CONTRIBUTE +# | +# '- ... <-- any language-specific files +# +# WHY PROJECT TEMPLATES +# +# Project templates CONFIGURE a language's tooling. Tooling +# is the catch-22 of learning a new language: you have to know +# the language to configure the tools, but you can't learn +# the language without first setting up the tools! When you +# start with a template, you can skip the weeks of trial-and-error +# that you would otherwise need to get started, because I stumbled +# through it for you. +# +# Each project template sets up the package managers, linters, +# formatters, build and test tools needed to get to "hello world" +# +# You can change the project template to configure the language +# tooling to your project's specific needs. +# +# HOW TO SET UP A PROJECT TEMPLATE +# +# Each language-specific folder contains a stubProject.nix. This +# nix file must contain the following nix expression: +# +# {pkgs ? import {}}: +# pkgs.writeShellApplication { +# name = "stubProject"; <- name of the executable. +# it MUST be named "stubProject" +# runtimeInputs = [ +# ... <- any packages needed to create +# and modify the project files +# ]; +# text = '' +# PROJECT_DIR="$1" <- relative path from root of repository +# to project (will usually be ) +# FLAKE_DIR="$2" <- relative path from project dir back to +# root of repository (will usually be ../) +# +# cat <<-EOT > "$PROJECT_DIR"/ <- logic to write project template files +# ... +# EOT +# ''; +# } +# +# This nix expression builds a script that is executed +# inside the project template directory. It is incredibly +# poweful. It can prompt for input. It can download files +# from the web, and it can read and modify ANY file in the +# monorepo. +# +# Use this power wisely. Do NOT delete or overwrite other +# project's files. +# +# this stubProject.nix composes the script in the language-specific +# stubProject.nix. It +# +# 1. creates project folder +# 2. whitelists project folder in monorepo root +# .gitignore +# 3. stubs README.md +# 4. stubs CONTRIBUTE.md +# 5. stubs .envrc +# 6. runs the language-specific stubProject.nix +# which stubs other project files and folders, +# and can modify README.md, CONTRIBUTE.md, and +# .envrc +# 7. creates a .gitignore and adds all stubbed +# project files to it +# + From a13893d6dae50eefa2cdd48272a7251c871bdb4e Mon Sep 17 00:00:00 2001 From: Ajay Ganapathy Date: Sun, 19 Oct 2025 21:29:37 -0400 Subject: [PATCH 07/18] chore: set up commitlint Signed-off-by: Ajay Ganapathy --- .config/.gitignore | 3 + .config/commitlintConfig.nix | 142 +++++++++++++++++++++ .config/lintCommit.nix | 239 +++++++++++++++++++++++++++++++++++ 3 files changed, 384 insertions(+) create mode 100644 .config/commitlintConfig.nix create mode 100644 .config/lintCommit.nix diff --git a/.config/.gitignore b/.config/.gitignore index ea16999..e583600 100644 --- a/.config/.gitignore +++ b/.config/.gitignore @@ -2,9 +2,12 @@ !language-nix !.envrc !.gitignore +!commitlintConfig.nix !configVscode.nix !importFromLanguageFolder.nix !configZed.nix !devShell.nix !sanitizeProjectName.nix !stubProject.nix +!CONTRIBUTE.md +!lintCommit.nix diff --git a/.config/commitlintConfig.nix b/.config/commitlintConfig.nix new file mode 100644 index 0000000..f505d39 --- /dev/null +++ b/.config/commitlintConfig.nix @@ -0,0 +1,142 @@ +# see: https://www.conventionalcommits.org/en/v1.0.0/#specification +# ___ ___ ________ ___ ___ ______________ _______ +# \ \ \ \ / ___ '. \ \ __ | | |_____ ____/ ___ \ +# \ \ \ \ \ \ \ \ \ \ | \ | | / / / / / / +# \ \___\ \ \ \ \ \ \ \ | \ | | / / / / / / +# \ ____ \ \ \ \ \ \ \' \' | / / / / / / +# \ \ \ \ \ \ \ \ \ | \ | / / / / / / +# \ \ \ \ \ '.___' \ \ | \ | / / / /___/ / +# \___\ \___\ '.________/ \___| \___| /___/ \________/ +# +# _______ _______ ___ ___ ___ ___ ____________ _____________ +# .' __ | .' ___ \ / | / | | \ | \ \____ ____\ ____ ____\ +# / / '--' / / / / / | / | | \ | \ \ \ \ \ +# / / / / / / / |/ /, | | \| \ \ \ \ \ +# / / / / / / / /\ / | | | |\ /\ \ \ \ \ \ +# / / __ / / / / / / \__/ | | | | \__/ \ \ \ \ \ \ +# / '___/ / / '--' / / / | | | | \ \ .---' '---. \ \ +# \_______.' \_______.' /___/ |___| |___| \___\ \___________\ \___\ +# +# +# What are you going to be doing with your life in two years? Five years? A decade? +# You probably don't know for sure. Sit with that uncertainty for a second. That is +# exactly how the person maintaining your code in the future will feel when they have +# to fix a bug you introduced. They won't know why you wrote the code you did, unless +# you tell them. When they exclaim "Why would anyone ever write this? What on earth +# was this person thinking?!", your commit message should give them a compelling answer. +# +# We use a simplified version of conventional commit +# +# -, +# chore: inject global logger into main actor |- header +# --^-- --^--------------------------------- -' +# type header message +# -, +# * Mock the logger in tests | +# * Verify log calls |- body +# * Eliminate file I/O during test runs | +# -' +# +# Think of each commit as a step in a tutorial. +# +# When I review your PRs, I will step through each of your commits. +# +# Your commit message should read like an instruction, and it should describe what +# your code does. I should be able to follow your instruction, and write a different +# implementation of the same functionality +# +# When you submit a PR, Each commit must be independently buildable and testable. +# +{pkgs ? import {}}: +(pkgs.formats.yaml {}).generate "conf.yml" { + version = "v0.10.1"; + formatter = "default"; + rules = [ + "header-min-length" + "header-max-length" + "body-max-line-length" + "footer-max-line-length" + "type-enum" + ]; + severity = { + default = "error"; + }; + settings = { + header-min-length = { + argument = 10; + }; + header-max-length = { + argument = 50; + }; + body-max-line-length = { + argument = 72; + }; + footer-max-line-length = { + argument = 72; + }; + type-enum = { + argument = [ + # we only use 3 of the 11 available commit types. Why? + # Because code shouldn't be complicated. Asking a programmer + # to choose between 11 different commit types forces them to + # perform 50 subjective comparisons every. time. they. commit. + # + # Asking a programmer to choose between just 3 commit + # types makes them perform just 3 subjective comparisons + # for each commit. Here's when you should use each type + # of commit: + # + # your commit + # ,------------------|------------------, + # | | | + # feat fix chore + # -------^-------- -------^-------- -------^-------- + # exports something fixes existing literally everything + # new. Adds to public API. else + # public API. + # + # Bumps major or Bumps patch + # minor version version number + # number + + "feat" # New features that add functionality. + + # "docs" # Documentation only changes. + # We do not use this, because you should update + # documentation in the same commit that you update + # code + + "chore" # Regular maintenance tasks, no production code change. + + # "style" # Changes that do not affect the meaning of the code + # (white-space, formatting, etc). We do not use this + # because you should NOT be changing the formatters. + # + # Doing so is a pet peeve of mine. Formatting changes + # shadow the git blame. They make it harder to understand + # who made a breaking change to the code, because a + # formatting change isn't a breaking change. + + # "refactor" # Code changes that neither fix a bug nor add a feature. + # "perf" # Changes that improve performance. + # "test" # Adding missing tests or correcting existing tests. + # "build" # Changes that affect the build system or external dependencies. + # "ci" # Changes to CI configuration files and scripts. + # + # These are all just other names for chores + + "fix" # Bug fixes and corrections. Try to avoid this. + # If you submit "fix" in a PR, I'm probably going to + # ask you to fixup your branch so that you don't need + # to commit the fix + # + # fix should only be used if we need to patch a production + # bug + + #"revert" # Reverts a previous commit + # This is just another name for fix. Don't commit bugs in + # the first place and you'll have nothing to revert + ]; + }; + }; +} diff --git a/.config/lintCommit.nix b/.config/lintCommit.nix new file mode 100644 index 0000000..71db107 --- /dev/null +++ b/.config/lintCommit.nix @@ -0,0 +1,239 @@ +# lint the commit message of the current commit +# +# HOW TO USE: +# +# To build this package, run: +# cd .config && nix-build commitlint.nix +# +# After building, you will see a result/ folder with this structure: +# +# result/ +# ├── bin/ +# │ └── commitlint (symlink to wrapper script) +# └── etc/ +# └── conf.yml (symlink to config) +# +# You can run the commitlint command with: +# ./result/bin/commitlint --help +# +{pkgs ? import {}}: let + config = import ./commitlintConfig.nix {inherit pkgs;}; + bin = pkgs.buildGoModule { + # see https://nixos.org/manual/nixpkgs/stable/#ssec-language-go + + pname = "commitlint-bin"; + version = "0.10.1"; + + # see https://github.com/conventionalcommit/commitlint/blob/bf3d490c7a9b64db7694eb16f02ce86c711aa3d7/.goreleaser.yml#L13C5-L13C258 + ldflags = [ + "-X github.com/conventionalcommit/commitlint/internal.version=v0.10.1" + "-X github.com/conventionalcommit/commitlint/internal.commit=e9a606ce7074ac884ea091765be1651be18356d4" + "-X github.com/conventionalcommit/commitlint/internal.buildTime=21082025" + ]; + + src = pkgs.fetchFromGitHub { + owner = "conventionalcommit"; + repo = "commitlint"; + rev = "e9a606ce7074ac884ea091765be1651be18356d4"; + hash = "sha256-OJCK6GEfs/pcorIcKjylBhdMt+lAzsBgBVUmdLfcJR0="; + }; + + vendorHash = "sha256-4fV75e1Wqxsib0g31+scwM4DYuOOrHpRgavCOGurjT8="; + }; + + # Wrapper script to run commitlint on the current commit, with config + wrapperScript = pkgs.writeShellApplication { + name = "lintCommit"; + runtimeInputs = [ + pkgs.git + bin + ]; + runtimeEnv = { + COMMITLINT_CONFIG = "${config}"; + }; + text = '' + MSG_FILE=''${*:-} + + if [ ! -f "$MSG_FILE" ]; then + # Use HEAD for the current commit if no message file is provided + git log -1 --pretty=%B HEAD > commit_message.txt + MSG_FILE=commit_message.txt + fi + + echo "$MSG_FILE" + + commitlint lint < "$MSG_FILE" + ''; + }; +in + pkgs.stdenv.mkDerivation { + name = "lintCommit"; + + # pkgs.stdenv.mkDerivation can copy files in from any folder. In this case, + # we have no files to copy in, because everything we want to use is already + # in the nix store + # + # When you define src, Nix copies the entire directory into the Nix store: + # + # ,-----------------, ,-----------------, + # | Source Directory | Nix Build Process | Nix Store Copy | + # | (mutable) | -------------------------> | (immutable) | + # | | | | + # | - Can change | | - Never changes | + # | - Not tracked | | - Hash-addressed| + # | - Local only | | - Distributable | + # '-----------------' '-----------------' + # + # WHY? This guarantees reproducibility and purity: + # 1. Prevents build-time changes to source affecting the result + # 2. Ensures identical inputs always produce identical outputs + # 3. Allows Nix to verify content with cryptographic hashes + # 4. Enables distribution and sharing of source code + # 5. Makes builds hermetic (isolated from the environment) + src = null; + + # pkgs.stdenv.mkDerivation runs autotools by default. autotools has many phases + # we don't actually need all of these phases when we wrap custom build scripts + # we disable all the phase swe don't need + phases = [ + # "unpackPhase" # Extracts source archives (tar, zip, etc.) into the build directory + # Default: Unpacks $src or sources listed in $srcs + + # "patchPhase" # Applies patches listed in $patches to the source code + # Default: Applies each patch in $patches with patch -p1 + + # "preConfigurePhase" # Runs before configuration, for pre-config preparations + # Default: Runs any preConfigure hooks and $preConfigurePhase + + # "configurePhase" # Runs ./configure or equivalent (cmake, meson, etc.) + # Default: Runs ./configure --prefix=$out with other standard flags + + # "preBuildPhase" # Runs before building, for last-minute setup + # Default: Runs any preBuild hooks and $preBuildPhase + + "buildPhase" # Normally compiles source code, in our case creates symlinks + # Default: Runs 'make' or equivalent build command + + # "checkPhase" # Runs the package's test suite to verify it works + # Default: Runs 'make check' if doCheck = true + + # "preInstallPhase" # Runs before installation + # Default: Runs any preInstall hooks and $preInstallPhase + + # "installPhase" # Copies built files to $out, creates directories as needed + # Default: Runs 'make install' or equivalent + + # "fixupPhase" # Post-processing: fixes shebangs, strips binaries, etc. + # Default: Runs numerous fixup steps like patchShebangs + + # "installCheckPhase" # Verifies the installation worked correctly + # Default: Runs 'make installcheck' if doInstallCheck = true + + # "distPhase" # Creates source distributions (tarballs, etc.) + # Default: Runs 'make dist' if doDist = true + ]; + + buildPhase = '' + # Create directories + mkdir -p $out/bin $out/etc + + # Create symlinks instead of copying to save on disk space + ln -s ${wrapperScript}/bin/lintCommit $out/bin/lintCommit + ln -s ${config} $out/etc/conf.yml + ''; + } +# COMPOSING DERIVATIONS +# +# Nix packages are built as "derivations" - the fundamental building blocks in the Nix ecosystem. +# Each derivation creates an isolated directory structure in the Nix store that contains directories +# with names similar to those in a traditional Linux system, such as: +# +# _______________________ +# | Typical Nix Derivation | +# | | +# | bin/ → Executables | +# | etc/ → Config files| +# | lib/ → Libraries | +# | include/ → Headers | +# | share/ → Data files | +# |________________________| +# +# see: https://tldp.org/LDP/Linux-Filesystem-Hierarchy/html/ +# +# When you build a derivation, Nix creates this structure at a unique path in the Nix store +# (e.g., /nix/store/-). This isolation ensures reproducibility and prevents +# conflicts between packages, unlike traditional Linux systems where packages install +# files into shared system directories. +# +# However, working with isolated components can be challenging. What if you need to +# combine multiple derivations into a cohesive whole? This is where composition comes in. +# +# Nix provides several mechanisms for composing derivations: +# +# ,-----------------, ,-----------------, +# | Derivation A | | Derivation B | +# | /bin/tool-a | | /etc/tool-b.conf| +# '-----------------' '-----------------' +# | | +# | | +# v v +# ,----------------------------, +# | Combined Derivation | +# | /bin/tool-a | +# | /etc/tool-b.conf | +# '----------------------------' +# +# Our commitlint example demonstrates this composition pattern: +# +# ______________________ _______________________ +# | commitlint-bin | | YAML Configuration | +# | | | | +# | /bin/commitlint ◀---|-----┐ | +# |______________________| | | +# | /conf.yml | +# |_______________________| +# | | +# | | +# v v +# ______________________ _______________________ +# | Wrapper Script | | Config Directory | +# | | | | +# | /bin/commitlint | | /etc/commitlint- | +# |______________________| | config.yml | +# | |_______________________| +# | | +# v v +# _________________________________________ +# | Final Package (stdenv.mkDerivation) | +# | | +# | /bin/commitlint → Wrapper Script | +# | that knows where to find: | +# | 1. The actual binary | +# | 2. The configuration file | +# | | +# | /etc/commitlint-config.yml → Config file| +# |_________________________________________| +# +# The key components in our example are: +# +# 1. Building the Go binary (bin) +# - Uses buildGoModule to compile the commitlint tool +# - Creates a derivation with just the binary +# +# 2. Generating the YAML config file (config) +# - Uses yamlFormatter.generate to create a structured config +# - Results in a file, not a complete derivation structure +# +# 3. Creating a wrapper script that: +# - Knows the exact paths to both the binary and config +# - Passes the right arguments to make them work together +# +# 4. Organizing the config file in a standard location: +# - Puts the config in /etc/ following filesystem conventions +# - Makes it discoverable and accessible +# +# 5. Combining everything with stdenv.mkDerivation: +# - Creates the directory structure we need +# - Uses symlinks to reference the actual files +# - Symlinks save disk space by avoiding file duplication + From 0a710d766c48a4d1aeae92fd6ab519c033eec2b5 Mon Sep 17 00:00:00 2001 From: Ajay Ganapathy Date: Thu, 4 Dec 2025 17:26:18 -0500 Subject: [PATCH 08/18] chore: set up recursive lint, build, test Signed-off-by: Ajay Ganapathy --- .config/.gitignore | 2 + .config/devShell.nix | 250 +++++++++++++++++++++++------- .config/language-nix/devShell.nix | 11 +- .config/recurse.nix | 147 ++++++++++++++++++ 4 files changed, 346 insertions(+), 64 deletions(-) create mode 100644 .config/recurse.nix diff --git a/.config/.gitignore b/.config/.gitignore index e583600..6bb3fe1 100644 --- a/.config/.gitignore +++ b/.config/.gitignore @@ -11,3 +11,5 @@ !stubProject.nix !CONTRIBUTE.md !lintCommit.nix +!recurse.nix +!getSemverTag.nix diff --git a/.config/devShell.nix b/.config/devShell.nix index 2cda871..7e3dcd7 100644 --- a/.config/devShell.nix +++ b/.config/devShell.nix @@ -41,7 +41,7 @@ devShellConfigs; wrappedPackages = devShellConfig: pkgs.lib.fix ( - let + self: let packages = builtins.listToAttrs ( builtins.map (package: { name = package.name; @@ -50,50 +50,169 @@ devShellConfig.packages ); name = devShellConfig.name; - in ( - self: - packages - // ( - if builtins.hasAttr "project-lint" packages - then { - project-lint = pkgs.writeShellApplication { - name = "project-lint"; - meta = packages.project-lint.meta; - text = '' - ${packages.project-lint}/bin/project-lint - ''; - }; - } - else throw "devShellConfig ${name} missing project-lint" - ) - // ( - if builtins.hasAttr "project-build" packages - then { - project-build = pkgs.writeShellApplication { - name = "project-build"; - meta = packages.project-build.meta; - text = '' - ${packages.project-build}/bin/project-build - ''; - }; - } - else throw "devShellConfig ${name} missing project-build" - ) - // ( - if builtins.hasAttr "project-test" packages - then { - project-test = pkgs.writeShellApplication { - name = "project-test"; - meta = packages.project-test.meta; - # todo pass a list of files that changed in current commit - text = '' - ${packages.project-test}/bin/project-test - ''; - }; - } - else throw "devShellConfig ${name} missing project-test" - ) - ) + getChanged = pkgs.writeShellApplication { + name = "getChanged"; + runtimeInputs = [pkgs.git]; + text = '' + # Get union of files changed in HEAD and uncommitted changes + # Limited to current working directory + + # Use associative array for deduplication + declare -A seen_files=() + + # Files changed in HEAD commit + while IFS= read -r -d "" file; do + seen_files["$file"]=1 + done < <(git diff --name-only -z HEAD~1 HEAD -- .) + + # Files with uncommitted changes (staged + unstaged) + while IFS= read -r -d "" file; do + seen_files["$file"]=1 + done < <(git diff --name-only -z HEAD -- .) + + # Get nested projects (directories with .envrc files) + declare -A nested_projects + while IFS= read -r -d "" subdir; do + nested_projects["$subdir"]=1 + done < <(fd --type f --hidden '.envrc' . --exec printf '%s\0' '{//}') + + # Remove files that are in nested projects + declare -A filtered_files + for file in "''${!seen_files[@]}"; do + file_dir=$(dirname "$file") + if [[ -z "''${nested_projects["$file_dir"]:-}" ]]; then + filtered_files["$file"]=1 + fi + done + + # Output unique filenames with null-byte separation for xargs -0 + printf "%s\0" "''${!filtered_files[@]}" + ''; + }; + getAll = pkgs.writeShellApplication { + name = "getAll"; + runtimeInputs = [pkgs.fd]; + text = '' + # list nested projects + declare -A nested_projects + while IFS= read -r -d "" subdir; do + nested_projects["$subdir"]=1 + done < <(fd --type f --hidden '.envrc' . --exec printf '%s\0' '{//}') + + # Build exclude arguments from nested_projects + exclude_args=() + for project in "''${!nested_projects[@]}"; do + exclude_args+=(--exclude "$project") + done + + # Associative array to store all files in project + declare -A files_in_project + + # Get all files excluding nested projects and gitignored files + while IFS= read -r -d "" file; do + files_in_project["$file"]=1 + done < <(fd "''${exclude_args[@]}" --type f --hidden --print0 .) + + # Output all collected files with null separator for xargs + printf "%s\0" "''${!files_in_project[@]}" + ''; + }; + in + packages + // ( + if builtins.hasAttr "project-lint" packages + then { + # wraps the project lint script. + # + # accepts $1 with "true" or "false", defaults to "false" + # + # $1="true": lint CHANGED files in project. this includes + # any uncommitted change + # + # $1="false": lint ALL files in project + # + # exits 0 if lint succeeds, 1 if it fails + project-lint = pkgs.writeShellApplication { + name = "project-lint"; + meta = packages.project-lint.meta; + runtimeInputs = [pkgs.git pkgs.findutils packages.project-lint getAll getChanged]; + text = '' + IGNORE_UNCHANGED="''${1:-false}" + + # project lint expects a list of files to lint as arguments + if [ "$IGNORE_UNCHANGED" = "true" ]; then + getChanged | xargs -0 -r project-lint || (echo "failed to lint $(realpath .)" >&2 && exit 1) + else + getAll | xargs -0 -r project-lint || (echo "failed to lint $(realpath .)" >&2 && exit 1) + fi + ''; + }; + } + else throw "devShellConfig ${name} missing project-lint" + ) + // ( + if builtins.hasAttr "project-build" packages + then { + # wraps the project build script. + # + # accepts $1 with "true" or "false", defaults to "false" + # + # $1="true": build CHANGED files in project. this includes + # any uncommitted change + # + # $1="false": build ALL files in project + # + # exits 0 if build succeeds, 1 if it fails + project-build = pkgs.writeShellApplication { + name = "project-build"; + meta = packages.project-build.meta; + runtimeInputs = [pkgs.git pkgs.findutils packages.project-build getAll getChanged]; + text = '' + IGNORE_UNCHANGED="''${1:-false}" + + # project-build expects a list of files to build as arguments + if [ "$IGNORE_UNCHANGED" = "true" ]; then + getChanged | xargs -0 -r project-build || (echo "failed to build $(realpath .)" >&2 && exit 1) + else + getAll | xargs -0 -r project-build || (echo "failed to build $(realpath .)" >&2 && exit 1) + fi + ''; + }; + } + else throw "devShellConfig ${name} missing project-build" + ) + // ( + if builtins.hasAttr "project-test" packages + then { + # wraps the project test script. + # + # accepts $1 with "true" or "false", defaults to "false" + # + # $1="true": test CHANGED files in project. this includes + # any uncommitted change + # + # $1="false": test ALL files in project + # + # Does not invoke the project test script if nothing has changed. + # Prints path to test artifacts, such as coverage reports, to stdout. + project-test = pkgs.writeShellApplication { + name = "project-test"; + meta = packages.project-test.meta; + runtimeInputs = [pkgs.git pkgs.findutils packages.project-test getAll getChanged]; + text = '' + IGNORE_UNCHANGED="''${1:-false}" + + # project-test expects a list of files to test as arguments + if [ "$IGNORE_UNCHANGED" = "true" ]; then + getChanged | xargs -0 -r project-test || (echo "failed to test $(realpath .)" >&2 && exit 1) + else + getAll | xargs -0 -r project-test || (echo "failed to test $(realpath .)" >&2 && exit 1) + fi + ''; + }; + } + else throw "devShellConfig ${name} missing project-test" + ) ); listBins = package: builtins.map (p: p.name) (builtins.filter (dirent: dirent.value != "directory") (pkgs.lib.attrsToList (builtins.readDir "${package}/bin"))); hasBins = package: pkgs.lib.pathExists "${package}/bin" && (builtins.length (listBins package) > 0); @@ -120,7 +239,7 @@ packages = with pkgs; [coreutils glow] ++ builtins.attrValues ( - # read the packages in devShellConfig, get the lint, lintSemVer, build, runTest, publishDryRun and publish packages and wrap them before re-emitting them into the list of packages + # read the packages in devShellConfig, get the project-lint, project-build, project-test packages and wrap them before re-emitting them into the list of packages wrappedPackages devShellConfig ); shellHook = let @@ -150,7 +269,26 @@ (import ./configVscode.nix {inherit pkgs;}) (import ./configZed.nix {inherit pkgs;}) ] - ++ (import ./stubProject.nix {inherit pkgs;}); + ++ (import ./stubProject.nix {inherit pkgs;}) + ++ builtins.map (cmd: + pkgs.writeShellApplication { + name = "${cmd}-all"; + meta = { + description = "${cmd} all projects"; + }; + runtimeInputs = [ + (import + ./recurse.nix + { + inherit pkgs; + steps = [cmd]; + }) + ]; + text = '' + IGNORE_UNCHANGED="''${1:-"true"}" + recurse "$IGNORE_UNCHANGED" + ''; + }) ["project-lint" "project-build" "project-test"]; commandDescriptions = writeCommandDescriptions p; in pkgs.mkShell { @@ -171,16 +309,7 @@ ''; }; }; -in { - inherit - # language-specific dev shell configs and function to turn dev shell configs into dev shells - # exported to make it possible for child flakes to inherit from dev shell configs and make dev shells - validDevShellConfigs - makeDevShell - # the language-specific dev shells and root dev shell, used in the root flake - devShells - ; -} +in {inherit validDevShellConfigs makeDevShell devShells;} # # LANGUAGE-SPECIFIC DEVELOPMENT SHELLS # @@ -299,7 +428,10 @@ in { # ... # packages used to lint project files # ]; # text = '' +# for file in "$@"; do # $@ is the list of files that have +# # changed since previous commit # ... # command used to lint project files +# done # ''; # }) # @@ -309,9 +441,13 @@ in { # description = "..." # description of what gets built # }; # runtimeInputs = with pkgs; [ +# for file in "$@"; do # $@ is list of files that have changed +# # since previous commit # ... # packages used to build project files +# done # ]; # text = '' +# # ... # command used to build project files # # command used to print project files # # to stdout, separated by null bytes diff --git a/.config/language-nix/devShell.nix b/.config/language-nix/devShell.nix index effa064..fa74a20 100644 --- a/.config/language-nix/devShell.nix +++ b/.config/language-nix/devShell.nix @@ -12,15 +12,12 @@ }; runtimeInputs = with pkgs; [ alejandra - fd + gnugrep + findutils ]; text = '' - # Find all .nix files and store in bash array - mapfile -d ''' -t nixfiles < <(fd --type f '\.nix$' -0) - - if [ ''${#nixfiles[@]} -gt 0 ]; then - alejandra -c "''${nixfiles[@]}" >&2 - fi + # Filter arguments to only .nix files and pass to alejandra + printf '%s\0' "$@" | grep -z '\.nix$' | xargs -0 -r alejandra -c ''; }) (pkgs.writeShellApplication diff --git a/.config/recurse.nix b/.config/recurse.nix new file mode 100644 index 0000000..b4b961c --- /dev/null +++ b/.config/recurse.nix @@ -0,0 +1,147 @@ +# recurse through the monorepo, linting, testing, building, and publishing every folder with an .envrc file in it +# +# this calls the lint, test, build and publish commands provided by a folder's respective .envrc +# +# ignore the root of the monorepo, when running this command, because root flake.nix also calls this command +# +# pass "false" as $1 to recurse through ALL projects, regardless of whether they have changed +# +# pass any additional arguments as $2... and they will be directly passed to steps as $1... +# +# if this script is built without any steps, it just prints the directories on which it would have run the steps +# to stdout +{ + pkgs ? import {}, + steps ? ["project-lint" "project-build" "project-test"], +}: let + # remove any invalid steps, and preserve the order of steps + validSteps = builtins.filter (step: + builtins.elem step [ + "project-lint" + "project-build" + "project-test" + ]) + steps; + msg = + if builtins.length validSteps > 0 + then + builtins.concatStringsSep ", " (builtins.map ( + step: + if step == "project-test" + then "testing" + else if step == "project-lint" + then "linting" + else "building" + ) + validSteps) + else ""; + recurse = pkgs.writeShellApplication { + name = "recurse"; + runtimeInputs = with pkgs; [ + fd + coreutils + git + direnv + glow + ]; + text = '' + if [ ! -d .git ]; then + echo "please run this script from the root of the monorepo" >&2 && exit 1 + fi + + IGNORE_UNCHANGED=''${1:-"true"} + + CWD=$(pwd) + + function check() { + local dir="$*" + cd "$dir" + + direnv allow + echo "${msg} $dir" >&2 + + # force rebuild of env flake + if [ -d ".direnv" ]; then + rm -rf ".direnv" + fi + + local failAt="" + + ${ + builtins.concatStringsSep "" ( + builtins.map ( + step: '' + if [ -z "$failAt" ] && ! direnv exec ./ ${step} "''${@:2}"; then + failAt="${step}" + fi + '' + ) + validSteps + ) + } + + # clear any .direnv so that other processes + # have a clean working dir + if [ -d ".direnv" ]; then + rm -rf ".direnv" + fi + + cd "$CWD" + + if [ -n "$failAt" ]; then + echo "error: ''${failAt} failed in ''${dir}" >&2 + return 1 + fi + } + + DIRS=() + + PROJECTS="projects that have changed" + + if [ "$IGNORE_UNCHANGED" = "true" ]; then + # Get all directories with .envrc files and check each for changes + while IFS= read -r -d "" envrc_file; do + envrc_dir="$(dirname "$envrc_file")" + # Check if anything changed in this directory between HEAD~1 and HEAD + if git diff --quiet HEAD~1 HEAD -- "$envrc_dir" && git diff --quiet HEAD -- "$envrc_dir"; then + # No changes in this directory + continue + else + # Directory has changes, add to array + if [ "$envrc_dir" != "$CWD" ]; then + DIRS+=("$envrc_dir") + fi + fi + done < <(fd --type f --hidden '.envrc' . --absolute-path --print0) + + else + while IFS= read -r -d "" envrc_file; do + envrc_dir="$(dirname "$envrc_file")" + if [ "$envrc_dir" != "$CWD" ]; then + DIRS+=("$envrc_dir") + fi + done < <(fd --type f --hidden '.envrc' . --absolute-path --print0) + PROJECTS="all projects" + fi + + + glow <<-EOF >&2 + ${msg} $PROJECTS: + + $(printf "%s\n" "''${DIRS[@]}") + + EOF + + # Process each directory + for dir in "''${DIRS[@]}"; do + if [ -n "$dir" ]; then + if ! check "$dir"; then + exit 1 + fi + fi + done + + ''; + }; +in + recurse From 7b74c9183580c37aa39d77e3bf14beec136b97ba Mon Sep 17 00:00:00 2001 From: Ajay Ganapathy Date: Sun, 21 Dec 2025 14:56:51 -0500 Subject: [PATCH 09/18] chore: set up git hooks recurse through all projects, running - project-lint - project-build - project-test Signed-off-by: Ajay Ganapathy --- .config/.gitignore | 1 + .config/devShell.nix | 2 ++ .config/installGitHooks.nix | 56 +++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 .config/installGitHooks.nix diff --git a/.config/.gitignore b/.config/.gitignore index 6bb3fe1..1a1f54d 100644 --- a/.config/.gitignore +++ b/.config/.gitignore @@ -10,6 +10,7 @@ !sanitizeProjectName.nix !stubProject.nix !CONTRIBUTE.md +!installGitHooks.nix !lintCommit.nix !recurse.nix !getSemverTag.nix diff --git a/.config/devShell.nix b/.config/devShell.nix index 7e3dcd7..d0b8cf2 100644 --- a/.config/devShell.nix +++ b/.config/devShell.nix @@ -268,6 +268,7 @@ [ (import ./configVscode.nix {inherit pkgs;}) (import ./configZed.nix {inherit pkgs;}) + (import ./installGitHooks.nix {inherit pkgs;}) ] ++ (import ./stubProject.nix {inherit pkgs;}) ++ builtins.map (cmd: @@ -302,6 +303,7 @@ project-install-vscode-configuration project-install-zed-configuration + project-install-git-hooks ${pkgs.glow}/bin/glow <<-'EOF' >&2 ${builtins.concatStringsSep "\n" commandDescriptions} diff --git a/.config/installGitHooks.nix b/.config/installGitHooks.nix new file mode 100644 index 0000000..1b36121 --- /dev/null +++ b/.config/installGitHooks.nix @@ -0,0 +1,56 @@ +{pkgs ? import {}, ...}: let + lintCommit = import ./lintCommit.nix {inherit pkgs;}; + commitMsg = "${lintCommit}/bin/lintCommit"; + prePush = "${(import ./recurse.nix { + inherit pkgs; + steps = ["project-lint" "project-build" "project-test"]; + })}/bin/recurse"; + + # Create the installer script + project-install-git-hooks = pkgs.writeShellApplication { + name = "project-install-git-hooks"; + meta = { + description = "Install commit-msg and pre-push hooks in this project. Automatically run when this shell is opened"; + }; + + runtimeInputs = [pkgs.coreutils]; + text = '' + # Check if .git/hooks exists + if [ ! -d .git/hooks ]; then + echo "❌ .git/hooks directory not found. Are you in a git repository?" >&2 + exit 1 + fi + + # Function to install a git hook with intelligent linking + install_hook() { + + local source="$1" + local dest="$2" + local hook_name="$3" + + if [ ! -e "$dest" ]; then + ln -sf "$source" "$dest" + echo "✅ linked $hook_name hook" >&2 + else + CURRENT_DIR=$(readlink -f "$dest") + if [ "$CURRENT_DIR" = "$source" ]; then + echo "✅ $hook_name hook already linked" >&2 + elif [ "$(dirname "$CURRENT_DIR")" = "$(dirname "$source")" ]; then + unlink "$dest" + ln -sf "$source" "$dest" + echo "✅ $hook_name hook updated" >&2 + else + ln -sf "$source" "$dest" + echo "✅ $hook_name hook replaced" >&2 + fi + fi + + } + + # Install hooks using the function + install_hook "${commitMsg}" ".git/hooks/commit-msg" "commit-msg" + install_hook "${prePush}" ".git/hooks/pre-push" "pre-push" + ''; + }; +in + project-install-git-hooks From 3ffc0964f634edfcd6c188aad9da2545c89e5d67 Mon Sep 17 00:00:00 2001 From: Ajay Ganapathy Date: Sun, 21 Dec 2025 19:17:41 -0500 Subject: [PATCH 10/18] chore: make github action push.yml every time a branch is pushed - project-lint - project-build - project-test Signed-off-by: Ajay Ganapathy --- .github/workflows/.gitignore | 3 + .github/workflows/push.yml | 271 +++++++++++++++++++++++++++++++++++ .gitignore | 2 + 3 files changed, 276 insertions(+) create mode 100644 .github/workflows/.gitignore create mode 100644 .github/workflows/push.yml diff --git a/.github/workflows/.gitignore b/.github/workflows/.gitignore new file mode 100644 index 0000000..8615428 --- /dev/null +++ b/.github/workflows/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!push.yml diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml new file mode 100644 index 0000000..14257c4 --- /dev/null +++ b/.github/workflows/push.yml @@ -0,0 +1,271 @@ +# Validate each commit in a push to ensure every commit maintains a working state +# across all supported platforms + +name: Validate Commits on Push + +# CI Pipeline Flow: +# +# This workflow validates each commit from base to head individually +# to ensure each commit maintains a working state: +# +# ┌──────────────────────────────────────────────────────────┐ +# │ Per-Commit Sequential Validation │ +# │ │ +# │ For each commit C1...CN: │ +# │ ┌─────────────────┐ ┌─────────────────┐ ┌────────┐ │ +# │ │ git checkout │ │ Run validation │ │ Next │ │ +# │ │ commit Ci ├──►│ step(s) ├──►│ commit │ │ +# │ └─────────────────┘ └─────────────────┘ └────────┘ │ +# └──────────────────────────────────────────────────────────┘ +# +# Workflow Steps: +# +# ┌──────────────────┐ +# │ │ +# │ Lint Commits │ <-- Run on Linux ARM +# │ Messages │ Validates commit message format +# │ │ for each commit in sequence +# └────────┬─────────┘ +# │ +# ▼ +# ┌──────────────────┐ +# │ │ +# │ Lint Projects │ <-- Run on Linux ARM +# │ │ Each commit is checked out and +# │ │ all projects are linted +# └────────┬─────────┘ +# │ +# ▼ +# │ Build & Test (parallel across platforms) +# │ ┌───────────────────────────────────────────────┐ +# │ │ Each commit is checked out and validated │ +# │ │ on every platform in parallel │ +# │ │ │ +# │ │ ┌─────────────────┐ │ +# │ │ │ macOS ARM64 │ │ +# │ │ │ ┌─────────────┐ │ │ +# │ │ │ │ Build │ │ │ +# └────┼─►│ │ Test │ │ │ +# │ │ └─────────────┘ │ │ +# │ └─────────────────┘ │ +# │ │ +# │ ┌─────────────────┐ ┌─────────────────┐ │ +# │ │ Linux ARM64 │ │ Linux x86_64 │ │ +# │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ +# │ │ │ Build │ │ │ │ Build │ │ │ +# │ │ │ Test │ │ │ │ Test │ │ │ +# │ │ └─────────────┘ │ │ └─────────────┘ │ │ +# │ └─────────────────┘ └─────────────────┘ │ +# │ │ +# └──────────────────────┬────────────────────────┘ +# │ +# │ +# ▼ +# ┌──────────────────┐ +# │ │ +# │ Summary Report │ +# │ │ +# └──────────────────┘ + +on: + push: + branches: + - "**" # Run on all branches + pull_request: + branches: ["main"] + +jobs: + # First job: Gather the commits that need validation + lint-commit-messages: + name: Lint Commit Messages + runs-on: ubuntu-22.04-arm # GitHub-hosted ARM64 runner + permissions: + id-token: "write" + contents: "read" + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: DeterminateSystems/nix-installer-action@main + + - name: Build lintCommit from Nix + run: | + # Build the lintCommit tool from the Nix expression + nix-build .config/lintCommit.nix + + - name: Lint Commit Messages + id: lint-commit-messages + run: | + # Exit at first failure + set -e + + # Get all commits from GitHub event + COMMITS=$(echo '${{ toJson(github.event.commits) }}' | jq -r '(. // []) | .[].id') + + # Handle empty pushes + if [[ -z "$COMMITS" ]]; then + echo "No commits to validate" + exit 0 + fi + + # Format for logging + COMMITS_JSON=$(echo "$COMMITS" | jq -R -s -c 'split("\n") | map(select(length > 0))') + echo "Found commits to validate: $COMMITS_JSON" + + # Loop through each commit + for commit in $COMMITS; do + echo "============================================================" + echo "🔍 Validating commit: $commit" + echo "📝 Message: $(git log -1 --pretty=%B $commit)" + echo "============================================================" + + # Get commit message and validate it using lintCommit + git log -1 --pretty=%B "$commit" > commit_message.txt + ./result/bin/lintCommit commit_message.txt + + echo "✅ Commit $commit passed validation" + echo "" + done + + # Lint job - only needs to run on one platform since it's not platform-specific + lint-projects: + name: Lint Projects + needs: lint-commit-messages + runs-on: ubuntu-22.04-arm # GitHub-hosted ARM64 runner + permissions: + id-token: "write" + contents: "read" + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: DeterminateSystems/nix-installer-action@main + + - name: Build recurse.nix script with project-lint step only + run: | + # Get nixpkgs directly from flake inputs + nix-build .config/recurse.nix --arg pkgs "import (builtins.getFlake \"path:$(pwd)\").inputs.nixpkgs {}" --arg steps '["project-lint"]' + + - name: Lint each commit + run: | + # Exit at first failure + set -e + + # Get all commits from GitHub event + COMMITS=$(echo '${{ toJson(github.event.commits) }}' | jq -r '(. // []) | .[].id') + + # Handle empty pushes + if [[ -z "$COMMITS" ]]; then + echo "No commits to validate" + exit 0 + fi + + # Loop through each commit + for commit in $COMMITS; do + echo "============================================================" + echo "🔍 Linting commit: $commit" + echo "📝 Message: $(git log -1 --pretty=%B $commit)" + echo "📅 Date: $(git log -1 --pretty=%ad --date=iso $commit)" + echo "🧑‍💻 Author: $(git log -1 --pretty=%an $commit)" + echo "============================================================" + + # Check out this specific commit + git checkout -q $commit + + # Run the recurse script which will lint + echo "🔍 Running lint on projects..." + temp_stderr=$(mktemp) + ./result/bin/recurse 2>"$temp_stderr" + exit_code=$? + while IFS= read -r line; do + echo " $line" >&2 + done < "$temp_stderr" + rm "$temp_stderr" + if [ $exit_code -ne 0 ]; then exit $exit_code; fi + + echo "✅ Commit $commit passed project linting" + echo "" + done + + # Return to the original branch/commit + git checkout -q ${{ github.sha }} + + # Build and test job - needs to run on all platforms + build-test: + name: Build & test on ${{ matrix.os-name }} (${{ matrix.platform }}) + needs: lint-projects + + strategy: + matrix: + include: + - os: macos-14 + os-name: macOS + platform: aarch64-darwin + - os: ubuntu-22.04 + os-name: Linux + platform: x86_64-linux + - os: ubuntu-22.04-arm + os-name: Linux + platform: aarch64-linux + + # Don't cancel other platform runs if one fails + fail-fast: false + + runs-on: ${{ matrix.os }} + permissions: + id-token: "write" + contents: "read" + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: DeterminateSystems/nix-installer-action@main + + - name: Build recurse.nix script with project-build and project-test steps + run: | + # Get nixpkgs directly from flake inputs + nix-build .config/recurse.nix --arg pkgs "import (builtins.getFlake \"path:$(pwd)\").inputs.nixpkgs {}" --arg steps '["project-build" "project-test"]' + + - name: Build and test each commit + run: | + # Exit at first failure + set -e + + # Get all commits from GitHub event + COMMITS=$(echo '${{ toJson(github.event.commits) }}' | jq -r '(. // []) | .[].id') + + # Handle empty pushes + if [[ -z "$COMMITS" ]]; then + echo "No commits to validate" + exit 0 + fi + + # Loop through each commit + for commit in $COMMITS; do + echo "============================================================" + echo "🔍 Validating commit: $commit on ${{ matrix.os-name }} (${{ matrix.platform }})" + echo "📝 Message: $(git log -1 --pretty=%B $commit)" + echo "📅 Date: $(git log -1 --pretty=%ad --date=iso $commit)" + echo "🧑‍💻 Author: $(git log -1 --pretty=%an $commit)" + echo "============================================================" + + # Check out this specific commit + git checkout -q $commit + + # Run the recurse script which will build and test + echo "🏗️ Running build and test on ${{ matrix.platform }}..." + temp_stderr=$(mktemp) + ./result/bin/recurse 2>"$temp_stderr" + exit_code=$? + while IFS= read -r line; do + echo " $line" >&2 + done < "$temp_stderr" + rm "$temp_stderr" + if [ $exit_code -ne 0 ]; then exit $exit_code; fi + + echo "✅ Commit $commit passed build and test on ${{ matrix.os-name }} (${{ matrix.platform }})" + echo "" + done + + # Return to the original branch/commit + git checkout -q ${{ github.sha }} diff --git a/.gitignore b/.gitignore index 5edd18e..f379076 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ * +!.github +!.github/** !.config !.config/** !.envrc From 450390d1eedb88eb8b54a9dcfbfdc50aad3cd4b5 Mon Sep 17 00:00:00 2001 From: Ajay Ganapathy Date: Sun, 19 Oct 2025 21:30:12 -0400 Subject: [PATCH 11/18] chore: make github action merge.yml do not allow a branch to be merged if it is not already up to date with the branch it is merging into Signed-off-by: Ajay Ganapathy --- .github/workflows/.gitignore | 1 + .github/workflows/merge.yml | 58 ++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 .github/workflows/merge.yml diff --git a/.github/workflows/.gitignore b/.github/workflows/.gitignore index 8615428..8d12dba 100644 --- a/.github/workflows/.gitignore +++ b/.github/workflows/.gitignore @@ -1,3 +1,4 @@ * !.gitignore !push.yml +!merge.yml diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml new file mode 100644 index 0000000..526e943 --- /dev/null +++ b/.github/workflows/merge.yml @@ -0,0 +1,58 @@ +name: Only allow fast-forward merge + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + enforce-rebase: + name: Enforce Rebase Requirement + runs-on: ubuntu-22.04-arm + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check if branch is rebased + run: | + # Exit at first failure + set -e + + # Get the target branch (base of the PR) + TARGET_BRANCH="${{ github.base_ref }}" + FEATURE_BRANCH="${{ github.head_ref }}" + + echo "============================================================" + echo "🔍 Checking rebase requirement" + echo "📥 Target branch: $TARGET_BRANCH" + echo "🌿 Feature branch: $FEATURE_BRANCH" + echo "============================================================" + + # Get the HEAD of the target branch + TARGET_HEAD=$(git rev-parse "origin/$TARGET_BRANCH") + echo "🎯 Target branch HEAD: $TARGET_HEAD" + + # Get the merge base between feature branch and target branch + MERGE_BASE=$(git merge-base "origin/$FEATURE_BRANCH" "origin/$TARGET_BRANCH") + echo "🔗 Merge base: $MERGE_BASE" + + # Compare them + if [[ "$TARGET_HEAD" == "$MERGE_BASE" ]]; then + echo "" + echo "✅ SUCCESS: $FEATURE_BRANCH can be merged into $TARGET_BRANCH" + else + echo "" + echo "❌ FAILURE: $FEATURE_BRANCH needs to be rebased onto $TARGET_BRANCH before it can be merged into it" + echo "" + echo "The feature branch is not up-to-date with the target branch." + echo "Before merging, please rebase your feature branch:" + echo "" + echo " git checkout $FEATURE_BRANCH" + echo " git fetch origin" + echo " git rebase origin/$TARGET_BRANCH" + echo " git push --force-with-lease" + echo "" + echo "This ensures a clean linear history without merge commits." + exit 1 + fi From bd16b78913fe1948ca07f0c34ff194f47812301b Mon Sep 17 00:00:00 2001 From: Ajay Ganapathy Date: Sun, 19 Oct 2025 21:30:19 -0400 Subject: [PATCH 12/18] chore: add codeowners - @designbyajay owns the entire repo Signed-off-by: Ajay Ganapathy --- .github/CODEOWNERS | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..1387aba --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,17 @@ +# CODEOWNERS file for repository-wide admin ownership +# +# This file defines who owns and is responsible for code in this repository. +# Repository admins are assigned as owners of all files to ensure proper +# oversight and approval for all changes. + +# Global ownership - all files require admin approval +* @designbyajay + +# You can also specify multiple admins if needed: +# * @admin @another-admin @third-admin + +# Alternative: If you're using GitHub teams instead of individual users: +# * @your-org/admin-team + +# Note: Replace @admin with actual GitHub username(s) or team name(s) +# Users/teams listed here must have write permissions to the repository From 86e6db2a5805660598acbb8a6b1603fa85ca7cbd Mon Sep 17 00:00:00 2001 From: Ajay Ganapathy Date: Mon, 15 Dec 2025 22:28:50 -0500 Subject: [PATCH 13/18] chore: add project-lint-semver command Signed-off-by: Ajay Ganapathy --- .config/devShell.nix | 204 +++++++++++++++++++++++++++++- .config/language-nix/devShell.nix | 40 ++++++ .config/recurse.nix | 5 +- 3 files changed, 247 insertions(+), 2 deletions(-) diff --git a/.config/devShell.nix b/.config/devShell.nix index d0b8cf2..e701cca 100644 --- a/.config/devShell.nix +++ b/.config/devShell.nix @@ -150,6 +150,208 @@ } else throw "devShellConfig ${name} missing project-lint" ) + // ( + if builtins.hasAttr "project-lint-semver" packages + then { + # wraps project-lint-semver script, which checks to make sure + # that the project's semantic version does not decrease from + # one commit to the next + # + # this script always prints the semantic version of each commit + # in the project to stderr e.g. + # + # version │ commit │ message + # ─────────┼───────────────────────────────┼──────────────────────────────── + # 0.0.0 │ 3b11f0b5ded8867c30e016b937e3e │ chore: add project-lint-semver + # │ 9bf1a3afb63 │ command + # 0.0.0 │ 46ac2a2488b8b72c49673e51a17d6 │ chore: set up recursive lint, + # │ 4a27b89c00d │ build, test + # 0.0.0 │ 1822aac4b623ef53fb087b88ee084 │ chore: make project-stub-nix + # │ 46121b1cd6d │ command + # 0.0.0 │ 31570c596591824dceed4192a3106 │ chore: set up zed + # │ 1f0317d3e6d │ configuration + # ... + # + # if the semantic version was bumped at HEAD, then this script + # prints the tag of the project i.e. + # + # path/to/project/vMAJOR.MINOR.PATCH + # + project-lint-semver = pkgs.writeShellApplication { + name = "project-lint-semver"; + meta = packages.project-lint-semver.meta; + runtimeInputs = [pkgs.git pkgs.glow packages.project-lint-semver]; + text = '' + # this script ALWAYS checks from HEAD commit + # all the way back to the base commit for the + # project + + SEMVER_LATEST="" + BUMPED=0 + + # $1 is semver + # return 0 if valid, 1 if invalid + # echoes $1 back if valid + function valid_semver(){ + if [[ ! "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + return 1 + fi + echo "$1" + } + + # $1 and $2 are semvers to compare + # returns 1 and echoes err msg if $1, $2 or both are invalid + # echo -1 if $1 less than $2 + # echo 0 if $1 equal to $2 + # echo 1 if $1 greater than $2 + function compare_semver(){ + + local left=() + local right=() + + if ! IFS="." read -ra left <<< "$(valid_semver "$1")" ; then + echo "error: \"$1\" is not a valid semantic version" + return 1 + fi + + if ! IFS="." read -ra right <<< "$(valid_semver "$2")" ; then + echo "error: \"$2\" is not a valid semantic version" + return 1 + fi + + if (( left[0] < right[0] )); then + echo -1 + return 0 + fi + + if (( left[0] > right[0] )); then + echo 1 + return 0 + fi + + if (( left[1] < right[1] )); then + echo -1 + return 0 + fi + + if (( left[1] > right[1] )); then + echo 1 + return 0 + fi + + if (( left[2] < right[2] )); then + echo -1 + return 0 + fi + + if (( left[2] > right[2] )); then + echo 1 + return 0 + fi + + echo 0 + return 0 + } + + function get_semver(){ + local sha="''${1:-}" + local sv + + if [ -z "$sha" ]; then + echo "expected a commit hash, received \"\"" + return 1 + fi + + if ! sv=$(project-lint-semver "$sha"); then + echo "$sv" + return 1 + fi + + echo "$sv" + } + + commits=() + ERR_SV="" + SV_PREV="" + + while IFS= read -r sha; do + if ! SV=$(get_semver "$sha"); then + ERR_SV="$SV" + break + fi + + if [ -n "$SV_PREV" ]; then + BUMPED=$(compare_semver "$SV_PREV" "$SV") + + if (( BUMPED < 0 )); then + ERR_SV="semantic version out of order: ''${SV_PREV} less than ''${SV}" + break + fi + fi + + SV_PREV="$SV" + + commits+=("$SV_PREV") + commits+=("$sha") + commits+=("$(git log -1 --pretty=%s "''$sha")") + + done < <(git rev-list HEAD -- .) + + SEMVER_LATEST="''${commits[0]}" + + if (( ''${#commits[@]} < 4 )); then + # only ONE commit, bumped must be 0 + BUMPED=0 + fi + + # Loop through commits array in groups of 3 + # + # Array structure: + # ┌─────────┬─────┬─────┬─────────┬─────┬─────┬─────────┬─────┬─────┐ + # │ semver1 │sha1 │msg1 │ semver2 │sha2 │msg2 │ semver3 │sha3 │msg3 │ + # └─────────┴─────┴─────┴─────────┴─────┴─────┴─────────┴─────┴─────┘ + # [0] [1] [2] [3] [4] [5] [6] [7] [8] + # + # Loop iterations: + # i=0: semver="''${commits [0]}", sha="''${commits [1]}", msg="''${commits [2]}" + # i=3: semver="''${commits [3]}", sha="''${commits [4]}", msg="''${commits [5]}" + # i=6: semver="''${commits [6]}", sha="''${commits [7]}", msg="''${commits [8]}" + + COMMITS_TABLE="" + + # Loop through commits array in groups of 3 + for (( i = 0; i < ''${#commits[@]}; i += 3 )); do + semver="''${commits[i]}" + sha="''${commits[i+1]}" + msg="''${commits[i+2]}" + + # Format each row as markdown table + COMMITS_TABLE+="| $semver | $sha | $msg |"$'\n' + done + + glow <<- EOF >&2 + | version | commit | message | + |:--------|:-------|:--------| + $COMMITS_TABLE + EOF + + if [ -n "$ERR_SV" ]; then + echo "^^^^" >&2 + echo "$ERR_SV" >&2 + fi + + if (( BUMPED > 0 )); then + echo "$(git rev-parse --show-prefix)''${SEMVER_LATEST}" + fi + + if [ -n "$ERR_SV" ]; then + exit 1 + fi + ''; + }; + } + else throw "devShellConfig ${name} missing project-lint-semver" + ) // ( if builtins.hasAttr "project-build" packages then { @@ -289,7 +491,7 @@ IGNORE_UNCHANGED="''${1:-"true"}" recurse "$IGNORE_UNCHANGED" ''; - }) ["project-lint" "project-build" "project-test"]; + }) ["project-lint" "project-lint-semver" "project-build" "project-test"]; commandDescriptions = writeCommandDescriptions p; in pkgs.mkShell { diff --git a/.config/language-nix/devShell.nix b/.config/language-nix/devShell.nix index fa74a20..6a144a1 100644 --- a/.config/language-nix/devShell.nix +++ b/.config/language-nix/devShell.nix @@ -20,6 +20,46 @@ printf '%s\0' "$@" | grep -z '\.nix$' | xargs -0 -r alejandra -c ''; }) + (pkgs.writeShellApplication { + name = "project-lint-semver"; + meta = { + description = "ensure the semantic version of a nix flake increases over time"; + runtimeInputs = with pkgs; [ + git + ]; + }; + text = '' + SHA="''${1:-}" + FIRST_LINE="" + PARSED_SEMVER="0.0.0" + + function parse_semver() { + if [[ "$FIRST_LINE" =~ ^#[[:space:]]+([0-9]+\.[0-9]+\.[0-9]+) ]]; then + PARSED_SEMVER="''${BASH_REMATCH[1]}" + fi + } + + # Get relative path from git root to current directory + RELATIVE_PATH=$(git rev-parse --show-prefix) + FLAKE_PATH="''${RELATIVE_PATH}flake.nix" + + if [ -n "$SHA" ]; then + # Get first line of flake.nix at specific SHA without changing working directory + if git cat-file -e "$SHA:$FLAKE_PATH" 2>/dev/null; then + FIRST_LINE=$(git show "$SHA:$FLAKE_PATH" | head -n1) + else + echo "No flake.nix found at SHA $SHA, using $PARSED_SEMVER" >&2 + FIRST_LINE="" + fi + else + FIRST_LINE=$(head -n1 flake.nix 2>/dev/null || echo "") + fi + + parse_semver + + echo "$PARSED_SEMVER" + ''; + }) (pkgs.writeShellApplication { name = "project-build"; diff --git a/.config/recurse.nix b/.config/recurse.nix index b4b961c..4f05a35 100644 --- a/.config/recurse.nix +++ b/.config/recurse.nix @@ -12,7 +12,7 @@ # to stdout { pkgs ? import {}, - steps ? ["project-lint" "project-build" "project-test"], + steps ? ["project-lint" "project-lint-semver" "project-build" "project-test"], }: let # remove any invalid steps, and preserve the order of steps validSteps = builtins.filter (step: @@ -20,6 +20,7 @@ "project-lint" "project-build" "project-test" + "project-lint-semver" ]) steps; msg = @@ -31,6 +32,8 @@ then "testing" else if step == "project-lint" then "linting" + else if step == "project-lint-semver" + then "linting semantic version of" else "building" ) validSteps) From e723300e3337e69431a0d6ba74ef00782535c9b1 Mon Sep 17 00:00:00 2001 From: Ajay Ganapathy Date: Sun, 14 Dec 2025 18:40:15 -0500 Subject: [PATCH 14/18] chore: run project-lint-semver in pre-push hook Signed-off-by: Ajay Ganapathy --- .config/installGitHooks.nix | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.config/installGitHooks.nix b/.config/installGitHooks.nix index 1b36121..2f07004 100644 --- a/.config/installGitHooks.nix +++ b/.config/installGitHooks.nix @@ -1,11 +1,16 @@ {pkgs ? import {}, ...}: let lintCommit = import ./lintCommit.nix {inherit pkgs;}; commitMsg = "${lintCommit}/bin/lintCommit"; - prePush = "${(import ./recurse.nix { - inherit pkgs; - steps = ["project-lint" "project-build" "project-test"]; - })}/bin/recurse"; - + prePush = "${pkgs.writeShellApplication { + name = "prePush"; + text = '' + # lint, lint-semver, build, test EVERYTHING before pushing + "${(import ./recurse.nix { + inherit pkgs; + steps = ["project-lint" "project-lint-semver" "project-build" "project-test"]; + })}/bin/recurse" false; + ''; + }}/bin/prePush"; # Create the installer script project-install-git-hooks = pkgs.writeShellApplication { name = "project-install-git-hooks"; From 0eb076aad39204386d1cfb88c76e0c7e04806aef Mon Sep 17 00:00:00 2001 From: Ajay Ganapathy Date: Sun, 14 Dec 2025 18:40:44 -0500 Subject: [PATCH 15/18] chore: run project-lint-semver in github on push Signed-off-by: Ajay Ganapathy --- .github/workflows/push.yml | 74 +++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 14257c4..7d60f62 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -30,6 +30,14 @@ name: Validate Commits on Push # ▼ # ┌──────────────────┐ # │ │ +# │ Lint Semantic │ <-- Run on Linux ARM +# │ Versions │ Validates semver in each project +# │ │ for each commit in sequence +# └────────┬─────────┘ +# │ +# ▼ +# ┌──────────────────┐ +# │ │ # │ Lint Projects │ <-- Run on Linux ARM # │ │ Each commit is checked out and # │ │ all projects are linted @@ -127,10 +135,74 @@ jobs: echo "" done + # Semantic version validation job + lint-semantic-versions: + name: Lint Semantic Versions + needs: lint-commit-messages + runs-on: ubuntu-22.04-arm # GitHub-hosted ARM64 runner + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: DeterminateSystems/nix-installer-action@main + + - name: Build recurse.nix script with project-lint-semver step only + run: | + # Get nixpkgs directly from flake inputs + nix-build .config/recurse.nix --arg pkgs "import (builtins.getFlake \"path:$(pwd)\").inputs.nixpkgs {}" --arg steps '["project-lint-semver"]' + + - name: Lint semantic versions for each commit + run: | + # Exit at first failure + set -e + + # we have to lint semantic versions at EVERY commit in a push + # to ensure that projects that were DELETED between base and + # HEAD have valid semantic versions + + # Get all commits from GitHub event + COMMITS=$(echo '${{ toJson(github.event.commits) }}' | jq -r '(. // []) | .[].id') + + # Handle empty pushes + if [[ -z "$COMMITS" ]]; then + echo "No commits to validate" + exit 0 + fi + + # Loop through each commit + for commit in $COMMITS; do + echo "============================================================" + echo "🔍 Linting semantic versions: $commit" + echo "📝 Message: $(git log -1 --pretty=%B $commit)" + echo "📅 Date: $(git log -1 --pretty=%ad --date=iso $commit)" + echo "🧑‍💻 Author: $(git log -1 --pretty=%an $commit)" + echo "============================================================" + + # Check out this specific commit + git checkout -q $commit + + # Run the recurse script which will lint semantic versions + echo "🔍 Running semantic version validation on projects..." + temp_stderr=$(mktemp) + ./result/bin/recurse 2>"$temp_stderr" + exit_code=$? + while IFS= read -r line; do + echo " $line" >&2 + done < "$temp_stderr" + rm "$temp_stderr" + if [ $exit_code -ne 0 ]; then exit $exit_code; fi + + echo "✅ Commit $commit passed semantic version validation" + echo "" + done + + # Return to the original branch/commit + git checkout -q ${{ github.sha }} + # Lint job - only needs to run on one platform since it's not platform-specific lint-projects: name: Lint Projects - needs: lint-commit-messages + needs: lint-semantic-versions runs-on: ubuntu-22.04-arm # GitHub-hosted ARM64 runner permissions: id-token: "write" From 12768d9e07c700a8798a24ad3d4de0b61cbcec79 Mon Sep 17 00:00:00 2001 From: Ajay Ganapathy Date: Sun, 21 Dec 2025 22:22:29 -0500 Subject: [PATCH 16/18] chore: set up tagMain github action tag main iterates through commits merged to main, and tags every commit that contains a project with a semver bump Signed-off-by: Ajay Ganapathy --- .github/workflows/.gitignore | 1 + .github/workflows/tagMain.yml | 115 ++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 .github/workflows/tagMain.yml diff --git a/.github/workflows/.gitignore b/.github/workflows/.gitignore index 8d12dba..aced676 100644 --- a/.github/workflows/.gitignore +++ b/.github/workflows/.gitignore @@ -2,3 +2,4 @@ !.gitignore !push.yml !merge.yml +!tagMain.yml diff --git a/.github/workflows/tagMain.yml b/.github/workflows/tagMain.yml new file mode 100644 index 0000000..72bd62e --- /dev/null +++ b/.github/workflows/tagMain.yml @@ -0,0 +1,115 @@ +name: Tag Main + +# Tag Main Pipeline Flow: +# +# This workflow iterates through commits from +# merge base to HEAD. For each commit, it runs recurse +# which outputs tags that should be created, then +# creates and pushes them atomically. + +on: + push: + branches: [main] + +jobs: + tag-main: + runs-on: ubuntu-latest + outputs: + tags: ${{ steps.collect-tags.outputs.tags }} + steps: + - uses: DeterminateSystems/nix-installer-action@main + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate GitHub App Token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Build recurse.nix script + run: | + nix-build .config/recurse.nix --arg pkgs "import (builtins.getFlake \"path:$(pwd)\").inputs.nixpkgs {}" --arg steps '["project-lint-semver"]' + + - name: Collect and push tags + id: collect-tags + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + set -e + + echo "============================================================" + echo "🔍 Processing commits from merge base to HEAD" + echo "============================================================" + + MERGE_BASE=${{ github.event.before }} + COMMITS=$(git rev-list --reverse $MERGE_BASE..HEAD) + + if [[ -z "$COMMITS" ]]; then + echo "ℹ️ No commits to process" + exit 0 + fi + + declare -a TAGS_TO_CREATE=() + + for commit in $COMMITS; do + echo "============================================================" + echo "🔍 Processing commit: $commit" + echo "📝 Message: $(git log -1 --pretty=%B $commit)" + echo "============================================================" + + git checkout -q $commit + + echo "🏃 Running recurse for commit $commit..." + + # recurse outputs tags (one per line) + while IFS= read -r tag; do + if [ -n "$tag" ]; then + # Store both tag and commit sha in array (alternating: tag, sha, tag, sha, ...) + TAGS_TO_CREATE+=("$tag" "$commit") + echo " 📌 Collected tag: $tag for commit: $commit" + fi + done < <(./result/bin/recurse) + + echo "✅ Commit $commit processed" + echo "" + done + + git checkout -q ${{ github.sha }} + + if [ -L result ]; then + unlink result + fi + + if [ ${#TAGS_TO_CREATE[@]} -gt 0 ]; then + echo "============================================================" + echo "📤 Creating ${#TAGS_TO_CREATE[@]} tags via API..." + echo "============================================================" + + # Iterate through pairs: tag at index i, sha at index i+1 + for ((i=0; i<${#TAGS_TO_CREATE[@]}; i+=2)); do + tag="${TAGS_TO_CREATE[$i]}" + sha="${TAGS_TO_CREATE[$((i+1))]}" + + echo "Creating tag: $tag for commit: $sha" + + if gh api repos/${{ github.repository }}/git/refs \ + -f ref="refs/tags/$tag" \ + -f sha="$sha" 2>&1; then + echo "✅ Created tag: $tag" + else + # Creation failed, check if tag already exists + if gh api repos/${{ github.repository }}/git/refs/tags/$tag &>/dev/null; then + echo " ℹ️ Tag already exists: $tag" + else + echo " ❌ Failed to create tag: $tag" >&2 + exit 1 + fi + fi + done + else + echo "ℹ️ No tags to create" + fi From b40e00a67edb73cd5be013515288f22e83a79b7c Mon Sep 17 00:00:00 2001 From: Ajay Ganapathy Date: Wed, 31 Dec 2025 20:53:12 -0500 Subject: [PATCH 17/18] chore: update documentation Signed-off-by: Ajay Ganapathy --- .config/CONTRIBUTE.md | 37 +++++ CONTRIBUTE.md | 373 ++++++++++++++++++++++++++++++++++++++++++ README.md | 43 +++++ 3 files changed, 453 insertions(+) create mode 100644 .config/CONTRIBUTE.md create mode 100644 CONTRIBUTE.md create mode 100644 README.md diff --git a/.config/CONTRIBUTE.md b/.config/CONTRIBUTE.md new file mode 100644 index 0000000..4cc1953 --- /dev/null +++ b/.config/CONTRIBUTE.md @@ -0,0 +1,37 @@ +The .config folder contains all of the code needed to support projects of various languages + +In this monorepo, we assume that each project is written in ONE language. + +To add support for projects in a new language, create a `language-*/` folder containing: + +``` +language-go/ + | + |-- .envrc # direnv integration: use flake ../../#go + | + |-- .gitignore # exclude everything except .nix files + | + |-- devShell.nix # project-lint, project-build, project-test commands + | + |-- configVscode.nix # LSP, formatter, extension settings for VSCode + | + |-- configZed.nix # LSP, formatter, extension settings for Zed + | + |-- stubProject.nix # script to scaffold new projects + | + '-- templateProject/ # files copied by stubProject.nix +``` + +Each nix expression points editors to nix-installed dev tools rather than +system-installed ones, ensuring consistent tooling across machines. + +**Reference Documentation:** + +| File | Documentation | Example | +| ------------------ | ------------------------------------------------------ | -------------------------------------------------------------- | +| `.envrc` | use `flake ../../#nix` | [language-nix/.envrc](language-nix/.envrc) | +| `.gitignore` | exclude all except `.nix` files | [language-nix/.gitignore](language-nix/.gitignore) | +| `devShell.nix` | see [devShell.nix](devShell.nix) lines 617-722 | [language-nix/devShell.nix](language-nix/devShell.nix) | +| `configVscode.nix` | see [configVscode.nix](configVscode.nix) lines 119-189 | [language-nix/configVscode.nix](language-nix/configVscode.nix) | +| `configZed.nix` | see [configZed.nix](configZed.nix) lines 146-213 | [language-nix/configZed.nix](language-nix/configZed.nix) | +| `stubProject.nix` | see [stubProject.nix](stubProject.nix) | [language-nix/stubProject.nix](language-nix/stubProject.nix) | diff --git a/CONTRIBUTE.md b/CONTRIBUTE.md new file mode 100644 index 0000000..b600231 --- /dev/null +++ b/CONTRIBUTE.md @@ -0,0 +1,373 @@ +# Contribute to Projects: + +> [!TIP] +> Before you contribute to any project in this monorepo, you must [install nix](https://determinate.systems/nix-installer/) and [nix-direnv](https://github.com/nix-community/nix-direnv). This monorepo uses nix and nix-direnv to automatically bootstrap all dev tools. Don't install the dev tools for these projects manually. Use the versions provided by nix. + +
+Why a monorepo? +TL;DR life is short. I don't have time to leapfrog between different repos, and neither do you. + +In many ways, programming is the pursuit of maximum efficiency. A skilled programmer doesn't just optimize the time complexity of the machine. They optimize the time complexity of their life. In this case, that means putting all the code we write in ONE place where we can all see it, debug it and reuse it. No messing with submodules, symlinks, or package proxies. +
+ +
+Why nix? +Nix is a _cross platform_, _deterministic_ build tool. With nix, if it works on your machine, it works on _all_ machines. + +Nix makes reproducible builds possible. This is very important when working with platform-specific, compiled code. Without nix, build scripts, such as makefiles, link against whatever libraries they find on your development machine. These libraries change from one OS to the next, making it difficult to build the same software on different machines. Nix versions libraries, and provides them directly to build scripts. + +``` + ;^'^i:r;, + ,nIix.l' p +n c +C nixpkgs D +`-._______,_____.-" + | + dev tools + | + ____V_____ + / | + | flake.nix | ,-----.________ + | | | | + | +---- copied ----> /nix/store | + | | into | | + |___________| '______,_______' + | + | + _______V_______ + | nix-direnv | + |_______,_______| + | + | + symlinked + into + ,-------------------------------' + _______V________ +| $PATH | +| | +| | +|________________| +``` + +Nix also makes makes reproducible development environments possible, without dev containers. Nix versions dev tools, such as npm, node, go, terraform, etc. in the same way that it versions libraries. + +Nix-direnv automatically loads these dev tools to your $PATH, when you `cd` into this repo. It unloads these tools when you `cd ..`. You don't have to globally install *any* dev tools (other than nix itself), you don't have to install version managers and you don't have to remember to switch between versions of tools when you switch between projects. +
+ +## Develop + +`cd` into the root of the repository. `nix-direnv` will load `.envrc`, which will install git hooks, dev tools, and print a list of helper commands. If you are developing in [vscode](https://code.visualstudio.com/download) or [zed](https://zed.dev), `nix-direnv` will also configure your editor settings and extensions. + +If you have installed [nix](https://docs.determinate.systems) and nix-direnv, you should see the following output: + +``` +✅ linked /nix/store/a6154vsavsldv23wwdwgb5q1hx7kly78-vscodeConfiguration to ./.vscode +✅ linked /nix/store/8s2f27ikvyxqf8mdzs4aa3p5jaa9cr05-zedConfiguration to ./.zed +✅ linked commit-msg hook +✅ linked pre-push hook + + project-install-vscode-configuration + + │ install .vscode/ configuration folder, if .vscode/ is not already present. + │ Automatically run when this shell is opened. + + project-install-zed-configuration + + │ install .zed/ configuration folder, if .zed/ is not already present. + │ Automatically run when this shell is opened + + project-install-git-hooks + + │ Install commit-msg and pre-push hooks in this project. Automatically run + │ when + │ this shell is opened + + project-stub-nix + + │ Stub a nix project + + project-lint-all + + │ project-lint all projects + + project-lint-semver-all + + │ project-lint-semver all projects + + project-build-all + + │ project-build all projects + + project-test-all + + │ project-test all projects +``` + +> [!TIP] +> if you did *not* install nix-direnv, you can run `nix develop` in the root of the repository to set up the development environment. + +`nix-direnv` automatically installs helper commands and IDE configuration files. If you are developing in VScode or Zed, restart your editor to pick up the configuration files. + +That's it! There's no super-complicated, error prone setup. No asking "what version of node do I use", and no debugging weird native build failures. Install nix, `cd` into this repo, and develop. + +### Repository Structure: + +This monorepo is split into several projects. Each project has a language, and bootstraps the dev tools you need to code in that language. E.g. some projects use go, and ship with the `go` command suite. Other projects use typescript and ship with `bun`. + +To load the dev tools, `cd` into the project. `nix-direnv` will read the `.envrc` in the project, and add the devtools to your $PATH. Then, it will print a list of commands you can use to lint, build and test the project. + +All projects will contain a `project-lint`, `project-build`, and `project-test` command. Some projects may contain additional commands. These helper commands wrap the language-specific commands required to lint, build and test the project. + +> [!TIP] +> **Why not directly run the language-specific lint, build and test commands?** +> You *can* run the project's language-specific lint, build and test commands! The project-lint, project-build, and project-test commands give the [git hooks](.config/installGitHooks.nix), [`project-lint-all`](.config/devShell.nix), [`project-lint-semver-all`](.config/devShell.nix), [`project-build-all`](.config/devShell.nix), [`project-test-all`](.config/devShell.nix), and [github actions](.github/workflows/push.yml) a project-agnostic command to call when they [recurse](.config/recurse.nix) through the projects in the monorepo. + +All projects contain a `project-lint-semver` command. This command checks the semantic version of a project, and verifies that its semantic versions do not decrease over the course of the project's git history. + +**This repository will prevent you from pushing obviously broken code to Github.** + +This repository [automatically runs](./.config/installGitHooks.nix) the `project-lint`, `project-lint-semver`, `project-build`, and `project-test` commands in every project in the *latest* commit, [before you push](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) commits to any remote, using the [pre-push hook](.config/installGitHooks.nix). It also runs these hooks on *every* commit you push in [github actions](.github/workflows/push.yml), every time you push to github. + +> [!WARN] +> This repository will NOT prevent you from pushing semantically incorrect code to Github. Your job is to test and bench your code thoroughly *before* you push. Don't make it the next programmer's responsibility to find out and fix your code's nasty side effects. + + ``` + projects/ + | -, + |- .config/ |- configuration files included in flake.nix + | -' + | -, + |- .github/ |- continuous integration configuration. Do not modify this. + | -' + | -, + |- .vscode | configuration files for zed, vscode and cursor. + | |- Do not modify these files. They are automatically + |- .zed | generated by flake.nix + | -' + | -, + |- .direnv | configuration and cache for nix-direnv. Do not modify + | |- these. + |- .envrc | + | -' + | -, + |- .gitignore |- list of files to ignore + | -' + | -, + |- flake.nix | installs dev environment when you `cd` into projects/ + | |- or open projects/ in zed editor. Auto-generates + |- flake.lock | editor configuration folders. + | -' + | -, + |- go-starter | + | | + |- typescript- |- example projects. Do not modify these. + | starter | + | | + | -' + | -, + |- infrastructure |- project that contains NixOS, NixOps, and Kubernetes code + | -' used to configure and deploy development hardware + : + : + ``` + +### When to make a new project: + +TL;DR: almost never. + +A project is a commitment to maintain a piece of code, indefinitely. When you make a project, it must have a stable API with 100% test coverage of all methods. + +This is a lot of extra work! Especially if no one else is using your project! + + ``` + projects/ + | + |- project-a/ + | + | + |- project-b/ + | + | + |- your-new-project/ + | | + | '- your-fancy-code <-- STOP. Don't do this + : + ``` +Instead of making a project, modify an existing project. Make sure you don't break its API. + + ``` + projects/ + | + |- project-a/ + | | + | '- your-fancy-code <- it's better to DUPLICATE + | the code between two packages + | than it is to commit to maintaining + |- project-b/ a third package. + | | + | '- your-fancy-code + | + : + : + ``` + +If you *think* you have a piece of code that can be shared between two existing projects, you probably don't. Just duplicate it in each existing project. In most cases, the piece of code will end up diverging over time, because the code will likely fulfill a different use case in each project. + +If the code does *not* diverge over time, it hasn't changed in at least 4 months, and other human beings want to use it, then it's a good candidate to refactor into its own project. + +I will only merge a PR with a project if +1. It contains 100% unit test coverage of all public APIs +2. Exposes some kind of documentation (e.g. if it is an API server, it must have an API.json). If it is a library, it must have auto-generated documentation. +3. It is referenced by at least THREE other projects. Ideally, one of the three projects should be in a repository *other than this monorepo.* +4. Contains a README and a CONTRIBUTE that follow the style guide prescribed by [stubProject.nix](./.config/stubProject.nix) +5. Has a public API that has been untouched in the past 4 months. + +### How to make a new project (if you *really* need to) + +`cd` to the root of this repository, and run one of the `project-stub-*` commands. For example, to create a nix project, run the `project-stub-nix` command. The command [will create a new project directory](./.config/stubProject.nix), complete with a development environment and all of the files needed to develop the project. + +> [!TIP] +> If you need to make a project that combines multiple languages, or has a complicated build process, you can create a nix project. The nix project lets you define your own custom lint, build and test scripts. + +### How to structure your code: + +Each file should contain ONE class, interface, or function. If your file exceeds 500 lines of code, your class, interface or function is probably doing too much. + +Do NOT shove multiple classes into a single file. If you do this, I WILL reject your PR, and ask you to split your code across multiple files. + +Each file should do exactly ONE thing. If a file is repeatedly modified in several commits, that is usually a sign that it is doing too much. + +In general, [follow design patterns](https://refactoring.guru) and the conventions for the language you are writing: + +- [typescript style guide](https://google.github.io/styleguide/tsguide.html) +- [go style guide](https://go.dev/doc/effective_go) + +If you do NOT follow these style guides, I will reject your PR and show you how to change your code so that it matches. + +Organize your code according to import scope. No code should ever import from a parent folder + +``` +GOOD: + +import code from ./path/to/code + +BAD: + +import code from ../../../code +``` + +When your code only imports from child folders, it prevents import cycles, and makes it easy for other contributors to reason about the dependencies. + +### How to author a commit: +See [`commitlint-config.nix`](.config/commitlintConfig.nix) + +### How to submit pull requests: + +The only way to contribute your code to the main branch is to submit a pull request. + +This repository will not let you merge your branch into main if the HEAD of main has diverged from the BASE of your branch. + +``` +Before pull request: + +main -----0-----1-----2-----3-----4-----5--. + \ + \ +your 6-----7-----8-----9 +branch + +After pull request: + +main -----0-----1-----2-----3-----4-----5--. .--10 + \ / ^^^ + \ / merge commit +your 6-----7-----8-----9 +branch + +``` + +Before your submit a pull request, make sure you rebase the main branch onto your branch, and resolve any conflicts. + +Once your pull request is approved, you can merge it into the the main branch. + +``` +Before rebasing +your branch onto +MAIN + +another +branch 6-----7-----8 + / \ + / \ +main -----0-----1-----2-----3-----4-----5--: '--9 <-- merge commit + \ + \ +your 6'----7'----8'----9' +branch ^^^ + cannot be merged, because HEAD of main is + commit 9, and base of your branch is commit 5. +After rebasing +your branch onto main: + +another +branch 6-----7-----8 + / \ + / \ +main -----0-----1-----2-----3-----4-----5--: '--9 <-- merge commit + \ + \ +your 10----11----12----13 +branch ^^^ + can be merged, because HEAD of main is + commit 9, and base of your branch is commit 9. + + +``` + +You must [sign all commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) in a pull request before you can merge it back into main. + +## Lint: + +- Run `project-lint-all` in the root of this repo to run ALL tests + +- Run `project-lint` inside a project to run the project's tests. + +## Build: + +- Run `project-build-all` in the root of this repo to run ALL tests + +- Run `project-build` inside a project to run the project's tests. + +## Test: + +- Run `project-test-all` in the root of this repo to run ALL tests + +- Run `project-test` inside a project to run the project's tests. + +Every exported function should have a unit test attached to it. + +## Document: + +When you stub a new project, it will create a README.md and a CONTRIBUTE.md. Follow the template instructions to document what the project does. + +Every public API, exported method, and script must describe its inputs, outputs, and any irreversible side effects. Use languages' standard tools to document the API contract (e.g. [api.json](https://swagger.io/specification/) for REST APIs, [go doc comments](https://go.dev/blog/godoc) for exported go functions, [TSdoc for typescript](https://tsdoc.org), etc. ) + +## Publish: + +All projects in this monorepo use [semantic versioning](https://semver.org/) (MAJOR.MINOR.PATCH format) with project-specific prefixes. + +To publish a project, bump its semantic version with the languages' respective tools. Then, submit a PR. Once I approve your PR, github actions will iterate through the commits in your PR, and tag the ones that contain semantic version bumps as path/to/project/vMAJOR.MINOR.PATCH + +- Multiple projects can be incremented in a single commit. In this case, each project will get its own tag, but all tags will point to the same commit. + +Once github actions tags the commits, it will publish projects to the registries that match their manifest files: + +e.g. + +| manifest file | registry | +|:---------------|:--------------------------------------------------------| +| `flake.nix` | [flakehub](https://flakehub.com/flakes) | +| `package.json` | [npm](https://www.npmjs.com/) | +| `go.mod` | [pkg.go.dev](https://pkg.go.dev/about#adding-a-package) | + +You cannot manually publish a project from your terminal. Only Github Actions has the keys to package registries. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd1476f --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Projects + +``` + ░▓▓█████████████▓▓ + ▓█████████████████████████▒ ░▓▓ + ▒███████████▓░ ▒▓▓░ ▓███████▓ + ▓████████▒ ▒█████████▒ + ░███████▒ ▒▓██████████████████▓▒ ▓███████░ + ██████▓ ░▓██████████████████████████████▒ ▓██████▓ + ▓█████▓ ▓█████████▓ ▓████████▓ ██████▒ + ▓█████ ▓███████▒ ▓▓████░ ▓▓ ▒███████▒ ██████ + ▓████▓ ███████ ▓███████████ ▒██████▓░ ▒██████░ ▓█████▒ + ▓█████▓ █████████▒ ▓████████ ▒█████▓ █████▓ + ▓███ ▓█████ ██████▓ ▒███▒ ▓█████ █████▒ + ▓████▓ █████▓ ░█████▓ ▓█████████▓ ▓█████ █████░ + █████ ▒█████ █████ ▓████████████████▓ █████ █████▒ ▓█████ +▓█████ ▓████▓ ▓████▓ ████▓ ▓████ ▓████▓ ▓████▓ ▓████ +▓█████ ▓████▓ ▓████▒ ████ ████ ▓████▓ ▓█████ +▒█████ ▒█████ ▒████▓ ████ ░█████ █████░ █████▓ ▒▓▓ + █████▒ █████▓ █████▓ ▓█▒ ████▓ █████▓ ░█████ █████▒ + ▓█████ ▒█████░ ▓█████▓ ▓█████▓ ░█████▓ ▓████▓ + ██████ █████▓ ░███████▒ ▓███████ ▓█████░ ░█████ + ██████ ██████░ ▓███████████████████████▒ ▓██████ ▒████░ + ▓█████ ▒██████▒ ░▓███████████████▒ ▓███████ + ░█████▓ ▓██████▒ ▒████████▓ ▒████▓ + ▓█████▓ ▓██▓ ▓█▓▒ ▓██████████░ ▓██████░ + ██████▓ █████████▓ ░██████████████▒ ███████░ + ███████▓ ▓█████▓ ░███████▓▒ ▓███████▓ + ▒████████▒ ▓████████▓ + ░██████████▓░ ▒▓██████████▓░ + ▓████████████████████████████████▒ + ░▓▓█████████████████▓▒ + +``` + + +The monorepo for all of incremental.design's open source projects. + +## How to use Projects: + +Each package in projects contains its own README with installation and usage instructions. In most cases, you will check out a package from a respective language's package manager, interact with an API or download a binary. You will only ever interact with this repository if you contribute to it. + +## [Contribute to projects:](CONTRIBUTE.md) From 1537a9c09067e47f99d3fdec2e45dbc116f39223 Mon Sep 17 00:00:00 2001 From: Ajay Ganapathy Date: Thu, 1 Jan 2026 16:03:27 -0500 Subject: [PATCH 18/18] fix: typo in flake.nix Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Ajay Ganapathy --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 986a1fe..e7ea914 100644 --- a/flake.nix +++ b/flake.nix @@ -1023,7 +1023,7 @@ # ----^----------- # no type declarations needed # -# Nix also has attribute sets, which are knows as maps +# Nix also has attribute sets, which are known as maps # or objects in other languages. # # Attribute sets are key-value collections, written with curly braces