Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9d1971d
chore: v13 overhaul — fix build/tests, modernize deps, bugs, docs, i18n
brmorillo Jun 17, 2026
f3919ed
test: raise unit coverage to ~98% lines / 93% branches
brmorillo Jun 17, 2026
8cbdaac
refactor!: standardize errors, signatures and names (BREAKING, v13)
brmorillo Jun 17, 2026
cb9d189
refactor!: pre-v13 audit hardening — security, validation, contracts …
brmorillo Jun 17, 2026
3e2db9e
docs: restructure into per-module docs, add CLAUDE.md, rewrite README
brmorillo Jun 17, 2026
cd45735
fix: timSort must not mutate input; isValidSnowflake accepts bigint
brmorillo Jun 17, 2026
0d65194
feat(sort): add additive inPlace option; test: lock project invariants
brmorillo Jun 17, 2026
c9c1c1f
refactor(object)!: unflattenObject is non-mutating by default (+inPlace)
brmorillo Jun 17, 2026
d012106
docs: refresh CLAUDE.md and docs index for v13 conventions
brmorillo Jun 17, 2026
932ac11
docs: reconcile every module doc with the v13 implementation
brmorillo Jun 17, 2026
c213e5d
feat: inPlace option on all data-transforming Array/Object methods
brmorillo Jun 17, 2026
dfefbe4
docs(CLAUDE): reflect expanded inPlace coverage and the two mutabilit…
brmorillo Jun 17, 2026
4343253
ci: add CI pipeline, gitleaks scan, and PR auto-versioning
brmorillo Jun 17, 2026
1a923a0
ci: add release/publish workflow, simplify gitleaks, set version base…
brmorillo Jun 17, 2026
c012069
docs: add TSDoc to all internal adapter classes and helpers
brmorillo Jun 17, 2026
92ab1f6
chore: pin all dependency versions to exact installed values
brmorillo Jun 17, 2026
32bfab1
docs: add contributing guidelines and dependency update policy to README
brmorillo Jun 17, 2026
a58009c
Merge remote-tracking branch 'origin/main' into chore/ci-and-pinning
brmorillo Jun 17, 2026
efc3d39
chore(release): 14.0.0
github-actions[bot] Jun 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: CI

on:
pull_request:
branches: [main]
push:
branches: [main]

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
quality:
name: Type-check, lint, test, build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Type-check
run: bun run type-check

- name: Lint
run: bun run lint

- name: Test (unit + integration, with coverage thresholds)
run: bun run test:ci

- name: Build (CJS + ESM + d.ts)
run: bun run build

secrets-scan:
name: Gitleaks secret scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Run gitleaks
run: |
set -euo pipefail
ver="$(curl -sSfL -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
https://api.github.com/repos/gitleaks/gitleaks/releases/latest \
| grep -oE '"tag_name": "v[^"]+' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)"
echo "Using gitleaks v${ver}"
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${ver}/gitleaks_${ver}_linux_x64.tar.gz" \
| tar -xz gitleaks
# Scans the checked-out tree (tracked files only — gitignored files like
# .env are never present in CI). Fails the job if any secret is found.
./gitleaks detect --source . --no-git --config .gitleaks.toml --redact --verbose --no-banner
68 changes: 68 additions & 0 deletions .github/workflows/pr-version.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: PR Version Bump

# Automatically bumps the package version (and CHANGELOG) from the conventional
# commits in the pull request, committing the bump back to the PR branch BEFORE
# the merge. When the PR is merged, main already carries the new version.

on:
pull_request:
types: [opened, synchronize, reopened]
branches: [main]

# Allow the workflow to push the bump commit back to the PR branch.
permissions:
contents: write

concurrency:
group: pr-version-${{ github.event.pull_request.number }}
cancel-in-progress: false

jobs:
bump:
name: Bump version from conventional commits
# GITHUB_TOKEN cannot push to a fork's branch — only run for same-repo PRs.
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
fetch-depth: 0
fetch-tags: true

- name: Skip if this PR already contains a release bump
id: guard
run: |
set -euo pipefail
if git log --pretty=%s "origin/${{ github.base_ref }}..HEAD" \
| grep -qiE '^chore\(release\)'; then
echo "A chore(release) commit already exists in this PR — skipping."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi

- name: Set up bun
if: steps.guard.outputs.skip == 'false'
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
if: steps.guard.outputs.skip == 'false'
run: bun install --frozen-lockfile

- name: Bump version + CHANGELOG
if: steps.guard.outputs.skip == 'false'
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Compute the next version from conventional commits, update package.json
# and CHANGELOG.md, and commit "chore(release): vX.Y.Z". Tagging happens
# at publish time, not here. Config lives in .versionrc.json.
bunx commit-and-tag-version --skip.tag

- name: Push the bump to the PR branch
if: steps.guard.outputs.skip == 'false'
run: git push origin "HEAD:${{ github.head_ref }}"
82 changes: 82 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Release

# When a release lands on main (the PR's "chore(release): vX.Y.Z" bump is merged),
# tag the commit, publish to npm, and create a GitHub release. This also anchors
# the next PR's version computation (commit-and-tag-version is tag-based).

on:
push:
branches: [main]

concurrency:
group: release
cancel-in-progress: false

permissions:
contents: write # create tags + GitHub releases
id-token: write # npm provenance

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true

- name: Read version from package.json
id: pkg
run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"

- name: Check whether this version is already tagged
id: tag
run: |
if git rev-parse "v${{ steps.pkg.outputs.version }}" >/dev/null 2>&1; then
echo "Tag v${{ steps.pkg.outputs.version }} already exists — nothing to release."
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi

- name: Set up bun
if: steps.tag.outputs.exists == 'false'
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Set up Node (for npm publish auth)
if: steps.tag.outputs.exists == 'false'
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'

- name: Install dependencies
if: steps.tag.outputs.exists == 'false'
run: bun install --frozen-lockfile

- name: Build
if: steps.tag.outputs.exists == 'false'
run: bun run build

- name: Create and push the tag
if: steps.tag.outputs.exists == 'false'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "v${{ steps.pkg.outputs.version }}" -m "v${{ steps.pkg.outputs.version }}"
git push origin "v${{ steps.pkg.outputs.version }}"

- name: Publish to npm
if: steps.tag.outputs.exists == 'false'
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Create GitHub release
if: steps.tag.outputs.exists == 'false'
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.pkg.outputs.version }}
generate_release_notes: true
26 changes: 26 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Gitleaks configuration for @brmorillo/utils.
#
# Uses the built-in default ruleset (≈150 well-tuned rules for AWS, GCP, GitHub,
# npm, Stripe, private keys, etc.) and allowlists this repo's test fixtures,
# examples and docs (which contain only throwaway sample values).
#
# Run locally with:
# gitleaks detect --source . --no-git --config .gitleaks.toml --redact --verbose
#
# Reference: https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml

title = "gitleaks config for @brmorillo/utils"

[extend]
useDefault = true

[allowlist]
description = "Test fixtures, examples and docs contain only throwaway sample values — not real secrets."
paths = [
'''(^|/)tests/''',
'''(^|/)examples/''',
'''(^|/)docs/''',
'''(^|/)usage-example\.js$''',
'''(^|/)CHANGELOG\.md$''',
'''(^|/)bun\.lock$''',
]
44 changes: 43 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,48 @@
# Changelog

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.

## [14.0.0](https://github.com/brmorillo/util/compare/v12.0.0...v14.0.0) (2026-06-17)


### ⚠ BREAKING CHANGES

* **object:** unflattenObject is non-mutating by default (+inPlace)
* pre-v13 audit hardening — security, validation, contracts (BREAKING)
* standardize errors, signatures and names (BREAKING, v13)

### Features

* inPlace option on all data-transforming Array/Object methods ([c213e5d](https://github.com/brmorillo/util/commit/c213e5d69b3c101ac8636a1f7ee701d68423ecb1))
* **sort:** add additive inPlace option; test: lock project invariants ([0d65194](https://github.com/brmorillo/util/commit/0d65194db70a93e8ec9d9cd42ed4f50de3cd5ea8))


### Bug Fixes

* dist path ([efd332e](https://github.com/brmorillo/util/commit/efd332e761ec752a9da36fcdf7215845507fc8c1))
* timSort must not mutate input; isValidSnowflake accepts bigint ([cd45735](https://github.com/brmorillo/util/commit/cd45735842787d9458dc30794c0331dbe92cda8f))


### Documentation

* add contributing guidelines and dependency update policy to README ([32bfab1](https://github.com/brmorillo/util/commit/32bfab15332815dd749e92ef616e17c27f75b8ea))
* add TSDoc to all internal adapter classes and helpers ([c012069](https://github.com/brmorillo/util/commit/c012069627c971ce9eda21f1fe2372c0abebc89b))
* **CLAUDE:** reflect expanded inPlace coverage and the two mutability invariants ([dfefbe4](https://github.com/brmorillo/util/commit/dfefbe46bc151dfc3bcd02b9c728c98fd1aa3971))
* reconcile every module doc with the v13 implementation ([932ac11](https://github.com/brmorillo/util/commit/932ac119a36f649648bda5a0a46fb152d89dd62f))
* refresh CLAUDE.md and docs index for v13 conventions ([d012106](https://github.com/brmorillo/util/commit/d012106ba4f0909dd61c38deeebe69ebb1b4a5f6))
* restructure into per-module docs, add CLAUDE.md, rewrite README ([3e2db9e](https://github.com/brmorillo/util/commit/3e2db9ef67d8f5d57e72248cc6e0a83ac3b0ba76))


### Code Refactoring

* **object:** unflattenObject is non-mutating by default (+inPlace) ([c9c1c1f](https://github.com/brmorillo/util/commit/c9c1c1f51f47ed07d1119ddafd31d296c39b361e))
* pre-v13 audit hardening — security, validation, contracts (BREAKING) ([cb9d189](https://github.com/brmorillo/util/commit/cb9d189da645e8110a7df7e8cb9041417d5bbe0d))
* standardize errors, signatures and names (BREAKING, v13) ([8cbdaac](https://github.com/brmorillo/util/commit/8cbdaac1a172906c3b53e12c8319ab5bb8324115))


### Tests

* raise unit coverage to ~98% lines / 93% branches ([f3919ed](https://github.com/brmorillo/util/commit/f3919eda563b0109fc186268738d4d97a2154e01))

## [11.3.0](https://github.com/brmorillo/util/compare/v11.2.3...v11.3.0) (2025-06-18)

Expand Down
11 changes: 10 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,16 @@ bun run format # prettier --write src

Notes:
- The local TypeScript compiler is `./node_modules/.bin/tsc` (a bare `npx tsc` hits a placeholder).
- There is currently **no CI workflow** in the repo (`.github/` was removed); run `build` + `test` + `lint` locally before publishing.

## CI/CD (`.github/workflows/`)

- **`ci.yml`** — on PRs to `main` and pushes to `main`: bun install, `type-check`, `lint`, `test:ci` (coverage gate), `build`, and a **gitleaks** secret scan (`.gitleaks.toml`: `useDefault = true` for the full built-in ruleset; test fixtures/examples/docs are allowlisted). `.env` is gitignored so it is never scanned in CI.
- **`pr-version.yml`** — on PR open/synchronize/reopen to `main`: computes the next version from the PR's conventional commits with `commit-and-tag-version` (config in `.versionrc.json`), updates `package.json` + `CHANGELOG.md`, and commits `chore(release): vX.Y.Z` **back to the PR branch** (no tag). Guard: skips if the PR already contains a release commit, so it bumps once per PR (re-trigger by removing that commit). Same-repo PRs only (`GITHUB_TOKEN` can't push to forks).
- **`release.yml`** — on push to `main`: if `package.json`'s version isn't tagged yet, it `git tag`s `vX.Y.Z`, **publishes to npm** (`npm publish --provenance --access public`, needs the `NPM_TOKEN` secret), and creates a GitHub release. The tag anchors the next PR's version computation (the bump is tag-based).

Release flow: open a PR → `pr-version` bumps the version on the PR → merge → `release` tags + publishes. The package version is intentionally the **last released** value between releases (e.g. `12.0.1`) so the PR bump computes the new one (`13.0.0`); do not hand-edit it ahead of the bump.

Required repo settings: **Actions → Workflow permissions → Read and write** (so `pr-version`/`release` can push), and a **`NPM_TOKEN`** Actions secret for publishing.

## Where to look

Expand Down
79 changes: 79 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,85 @@ bun run lint # lint
bun run format # format with Prettier
```

## Contributing

All contributions must follow these conventions:

- **Commit messages** — [Conventional Commits](https://www.conventionalcommits.org/):
`feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`, `perf:`.
Breaking changes get a `!` suffix (`feat!:`, `refactor!:`) and a `BREAKING CHANGE:` footer.
- **Branch naming** — `feature/<short-name>`, `fix/<issue>`, `docs/<topic>`, `chore/<task>`.
- **PR process** — open a PR against `main`; the CI pipeline (type-check → lint → test with
coverage gate → build → secret scan) must pass. The `pr-version` workflow automatically commits
the version bump (`chore(release): vX.Y.Z`) to your branch before merge.
- **v13 API contract** — this is a stable, API-frozen line. Only additive, non-breaking changes
are accepted: new methods, new optional parameters, new exports. Signature changes, removals, or
observable behavior changes require a new major version.
- **Mutability convention** — data-transforming methods must be non-mutating by default. Opt-in
mutation is exposed via `inPlace?: boolean` (default `false`). Both invariant test suites
(`immutability.spec.ts` and `inplace-invariant.spec.ts`) must stay green.
- **Typed errors only** — never `throw new Error(...)`. Use the typed errors from `src/errors`.
- **TSDoc on every public member** — all public methods, constructors, and exported helpers must
have a `/** */` block with at least a summary line.
- **English only** — all identifiers, comments, doc strings, and test descriptions must be
in English.

## Dependency update policy

All runtime and development dependencies are pinned to **exact versions** (no `^`, `~`, or
`latest`) in `package.json`. This ensures fully reproducible installs and makes every dependency
change an intentional, reviewable commit.

### Update schedule

Dependency updates are performed **once a month**, on the first working day of each month.

### Version lag — 3-month rule

We intentionally stay **at least 3 months behind the latest published version** of every
dependency. This buffer gives the community time to discover and disclose supply-chain attacks,
malicious publishes, and critical regressions before we adopt them.

> Example: if `axios` publishes `2.0.0` on 1 March 2025, the earliest we adopt it is
> 1 June 2025.

### How to update a dependency

1. Check whether the target version is at least 3 months old:
```bash
npm view <package> time --json # lists publish timestamps for every version
```
2. Read the changelog and check for breaking changes, CVEs, or supply-chain advisories.
3. Manually edit the version string in `package.json` (no `^` or `~`).
4. Run `bun install` to refresh `bun.lock`.
5. Run the full test suite and build:
```bash
bun run build
CI=true bun run test:ci
```
6. Commit with:
```
chore(deps): update <package> from X.Y.Z to A.B.C
```
7. Open a PR. The CI pipeline validates the updated lockfile automatically.

### CJS-compatibility check

Before bumping any **runtime** dependency to a new major, verify it still ships a CommonJS
build:

```bash
node -e "require('<package>')" # must not throw
node -p "Object.keys(require('<package>/package.json').exports)" # must include 'require'
```

The following packages are permanently pinned to a specific major for CJS compatibility:

| Package | Pinned major | Reason |
| --- | --- | --- |
| `uuid` | 11.x | v14+ is ESM-only |
| `@paralleldrive/cuid2` | 2.x | v3+ is ESM-only |

## License

MIT © [Bruno Morillo](https://github.com/brmorillo)
Loading