From 9754a6adf29caaeaac0081837f3a661600ef1a21 Mon Sep 17 00:00:00 2001 From: Filipe Forattini Date: Sat, 5 Sep 2026 10:55:58 -0300 Subject: [PATCH 1/3] ci: do not run developer git hooks in the release workflow The changesets action pushes the version commit and the tag; each push runs the husky pre-push hook, a typecheck of all 37 packages, for a commit that only bumps versions. The runner killed it mid-run and the release PR was never regenerated. HUSKY=0 for that job: hooks are for developers, and this workflow's inputs were typechecked on the PR that produced them. Claude-Session: https://claude.ai/code/session_014XwJPDhq1ahcm3rd454WzQ --- .github/workflows/red-release.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/red-release.yml b/.github/workflows/red-release.yml index 32d4a8276621..8696ae377c82 100644 --- a/.github/workflows/red-release.yml +++ b/.github/workflows/red-release.yml @@ -17,6 +17,12 @@ jobs: permissions: contents: write pull-requests: write + env: + # The version commit and the tag are pushed by the changesets action, and a push runs the + # husky pre-push hook: a typecheck of every package, for a commit that only bumps versions. + # It was killed for memory mid-run and took the release with it. Hooks are for developers; + # this workflow's inputs were typechecked on the PR that produced them. + HUSKY: 0 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: From e8247ecdb1677ff95aa2b0fff1c779332f6852e2 Mon Sep 17 00:00:00 2001 From: Filipe Forattini Date: Sat, 5 Sep 2026 11:12:52 -0300 Subject: [PATCH 2/3] ci: no more pre-push hook; typecheck in five shards on the runners The husky pre-push hook typechecked all 37 packages on every push. It was too heavy to live with on a laptop, and on a 4-vCPU runner it was killed mid-run and took a release with it. Gone, with husky itself. The typecheck workflow now runs the workspace in five shards, one job each, assigned by sorted package name so a failure names the package. The runners typecheck; the laptop does not. Claude-Session: https://claude.ai/code/session_014XwJPDhq1ahcm3rd454WzQ --- .github/workflows/red-release.yml | 6 ---- .github/workflows/typecheck.yml | 10 ++++++- .husky/pre-push | 20 ------------- bun.lock | 21 ++++++------- package.json | 2 -- script/typecheck-shard.ts | 50 +++++++++++++++++++++++++++++++ 6 files changed, 68 insertions(+), 41 deletions(-) delete mode 100755 .husky/pre-push create mode 100644 script/typecheck-shard.ts diff --git a/.github/workflows/red-release.yml b/.github/workflows/red-release.yml index 8696ae377c82..32d4a8276621 100644 --- a/.github/workflows/red-release.yml +++ b/.github/workflows/red-release.yml @@ -17,12 +17,6 @@ jobs: permissions: contents: write pull-requests: write - env: - # The version commit and the tag are pushed by the changesets action, and a push runs the - # husky pre-push hook: a typecheck of every package, for a commit that only bumps versions. - # It was killed for memory mid-run and took the release with it. Hooks are for developers; - # this workflow's inputs were typechecked on the PR that produced them. - HUSKY: 0 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index fa41b0906a79..aec9fe9c4917 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -9,7 +9,12 @@ on: jobs: typecheck: + name: typecheck (${{ matrix.shard }}/5) runs-on: blacksmith-4vcpu-ubuntu-2404 + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4, 5] steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 @@ -17,5 +22,8 @@ jobs: - name: Setup Bun uses: ./.github/actions/setup-bun + # One slice of the workspace per job: all 37 packages at once is more than a 4-vCPU runner + # can hold, and the job that tried used to die mid-run. Assignment is by sorted package + # name (see script/typecheck-shard.ts), so a failure names the package. - name: Run typecheck - run: bun typecheck + run: bun script/typecheck-shard.ts $((${{ matrix.shard }} - 1)) 5 diff --git a/.husky/pre-push b/.husky/pre-push deleted file mode 100755 index 5d3cc53411be..000000000000 --- a/.husky/pre-push +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh -set -e -# Check if bun version matches package.json -# keep in sync with packages/script/src/index.ts semver qualifier -bun -e ' -import { semver } from "bun"; -const pkg = await Bun.file("package.json").json(); -const expectedBunVersion = pkg.packageManager?.split("@")[1]; -if (!expectedBunVersion) { - throw new Error("packageManager field not found in root package.json"); -} -const expectedBunVersionRange = `^${expectedBunVersion}`; -if (!semver.satisfies(process.versions.bun, expectedBunVersionRange)) { - throw new Error(`This script requires bun@${expectedBunVersionRange}, but you are using bun@${process.versions.bun}`); -} -if (process.versions.bun !== expectedBunVersion) { - console.warn(`Warning: Bun version ${process.versions.bun} differs from expected ${expectedBunVersion}`); -} -' -bun typecheck diff --git a/bun.lock b/bun.lock index d2374a2c6127..67a2a345a93b 100644 --- a/bun.lock +++ b/bun.lock @@ -19,7 +19,6 @@ "@types/mime-types": "3.0.1", "@typescript/native-preview": "catalog:", "glob": "13.0.5", - "husky": "9.1.7", "oxlint": "1.60.0", "oxlint-tsgolint": "0.21.0", "prettier": "3.6.2", @@ -30,7 +29,7 @@ }, "packages/app": { "name": "@reddb-io/redcode-app", - "version": "1.18.20", + "version": "1.19.0", "dependencies": { "@corvu/drawer": "catalog:", "@dnd-kit/abstract": "0.5.0", @@ -97,7 +96,7 @@ }, "packages/cli": { "name": "@reddb-io/redcode-cli", - "version": "1.18.20", + "version": "1.18.21", "bin": { "lildax": "./bin/lildax.cjs", }, @@ -291,7 +290,7 @@ }, "packages/core": { "name": "@reddb-io/redcode-core", - "version": "1.19.0", + "version": "1.20.0", "dependencies": { "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/amazon-bedrock": "4.0.112", @@ -464,7 +463,7 @@ }, "packages/enterprise": { "name": "@reddb-io/redcode-enterprise", - "version": "1.18.20", + "version": "1.18.21", "dependencies": { "@hono/standard-validator": "catalog:", "@pierre/diffs": "catalog:", @@ -605,7 +604,7 @@ }, "packages/redcode": { "name": "@reddb-io/redcode", - "version": "0.15.0", + "version": "0.16.0", "bin": { "redcode": "./bin/redcode", }, @@ -770,7 +769,7 @@ }, "packages/sdk-next": { "name": "@reddb-io/redcode-sdk-next", - "version": "1.18.20", + "version": "1.18.21", "dependencies": { "@reddb-io/redcode-client": "workspace:*", "@reddb-io/redcode-core": "workspace:*", @@ -800,7 +799,7 @@ }, "packages/server": { "name": "@reddb-io/redcode-server", - "version": "1.18.20", + "version": "1.18.21", "dependencies": { "@reddb-io/multi-rpc": "0.29.6", "@reddb-io/redcode-core": "workspace:*", @@ -818,7 +817,7 @@ }, "packages/session-ui": { "name": "@reddb-io/redcode-session-ui", - "version": "1.18.20", + "version": "1.18.21", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", @@ -967,7 +966,7 @@ }, "packages/tui": { "name": "@reddb-io/redcode-tui", - "version": "1.18.20", + "version": "1.19.0", "dependencies": { "@opentui/core": "catalog:", "@opentui/keymap": "catalog:", @@ -4123,8 +4122,6 @@ "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], - "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], - "i18n-iso-countries": ["i18n-iso-countries@7.14.0", "", { "dependencies": { "diacritics": "1.3.0" } }, "sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg=="], "i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="], diff --git a/package.json b/package.json index 2a45cd718756..a46017023ddc 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "typecheck": "bun turbo typecheck", "upgrade-opentui": "bun run script/upgrade-opentui.ts", "postinstall": "bun run --cwd packages/core fix-node-pty", - "prepare": "husky", "random": "echo 'Random script'", "sso": "aws sso login --sso-session=opencode --no-browser", "translate:app": "bun run script/translate-app.ts", @@ -105,7 +104,6 @@ "@types/mime-types": "3.0.1", "@typescript/native-preview": "catalog:", "glob": "13.0.5", - "husky": "9.1.7", "oxlint": "1.60.0", "oxlint-tsgolint": "0.21.0", "prettier": "3.6.2", diff --git a/script/typecheck-shard.ts b/script/typecheck-shard.ts new file mode 100644 index 000000000000..1350a2d67f06 --- /dev/null +++ b/script/typecheck-shard.ts @@ -0,0 +1,50 @@ +#!/usr/bin/env bun +/** + * Typecheck one slice of the workspace. + * + * Every package at once needs more memory than a 4-vCPU runner has, and the pre-push hook that + * used to do exactly that was killed mid-run often enough to lose a release. So CI runs the + * packages in a fixed number of shards, each its own job: `bun script/typecheck-shard.ts 2 5` + * runs the third of five. The assignment is by sorted package name, so it is stable across runs + * and a failure names the package, not the shard. + */ +import path from "path" + +const [indexArg, totalArg] = process.argv.slice(2) +const index = Number(indexArg) +const total = Number(totalArg) +if (!Number.isInteger(index) || !Number.isInteger(total) || total < 1 || index < 0 || index >= total) { + console.error("usage: bun script/typecheck-shard.ts ") + process.exit(2) +} + +const root = path.resolve(import.meta.dir, "..") +const manifest = (await Bun.file(path.join(root, "package.json")).json()) as { + workspaces: string[] | { packages: string[] } +} +const patterns = Array.isArray(manifest.workspaces) ? manifest.workspaces : manifest.workspaces.packages + +// A Set, because the workspace globs overlap (`packages/*` and `packages/slack`). +const packages = new Set() +for (const pattern of patterns) { + const glob = new Bun.Glob(path.posix.join(pattern, "package.json")) + for await (const file of glob.scan({ cwd: root, dot: false })) { + if (file.includes("node_modules/")) continue + const json = (await Bun.file(path.join(root, file)).json()) as { name?: string; scripts?: Record } + if (json.name && json.scripts?.typecheck) packages.add(json.name) + } +} +const sorted = [...packages].sort() + +const mine = sorted.filter((_, i) => i % total === index) +if (mine.length === 0) { + console.log(`shard ${index + 1}/${total}: nothing to check`) + process.exit(0) +} +console.log(`shard ${index + 1}/${total}: ${mine.join(", ")}`) + +const proc = Bun.spawn(["bun", "turbo", "typecheck", ...mine.flatMap((name) => ["--filter", name])], { + cwd: root, + stdio: ["inherit", "inherit", "inherit"], +}) +process.exit(await proc.exited) From 39f5a7826df1cb8c098fd4ec66715abd9feb4e36 Mon Sep 17 00:00:00 2001 From: Filipe Forattini Date: Sat, 5 Sep 2026 12:05:19 -0300 Subject: [PATCH 3/3] ci: keep a check named typecheck for branch protection Branch protection requires a status check called "typecheck"; the matrix renamed the jobs to "typecheck (n/5)", so a green PR stayed blocked. A gate job with the old name now depends on every shard and passes only when they all did. Claude-Session: https://claude.ai/code/session_014XwJPDhq1ahcm3rd454WzQ --- .github/workflows/typecheck.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index aec9fe9c4917..0dcdebf039d2 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -8,7 +8,7 @@ on: workflow_dispatch: jobs: - typecheck: + shard: name: typecheck (${{ matrix.shard }}/5) runs-on: blacksmith-4vcpu-ubuntu-2404 strategy: @@ -27,3 +27,16 @@ jobs: # name (see script/typecheck-shard.ts), so a failure names the package. - name: Run typecheck run: bun script/typecheck-shard.ts $((${{ matrix.shard }} - 1)) 5 + + # The one check branch protection requires. It passes only when every shard did, so the + # protection rule keeps its name and the matrix can change shape underneath it. + typecheck: + name: typecheck + runs-on: ubuntu-latest + needs: shard + if: always() + steps: + - name: Every shard passed + run: | + echo "shards: ${{ needs.shard.result }}" + test "${{ needs.shard.result }}" = "success"