Restructure the repository into a pnpm workspace with the package under packages/trilean - #13
Merged
Merged
Conversation
Mearman
marked this pull request as ready for review
September 3, 2026 10:37
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Every file belonging to the package itself -- src/, test/, scripts/, its manifest, tsconfigs, tsdown/vitest/wrangler configs, its ESLint config, README, and CHANGELOG -- moves under packages/trilean/, leaving the repository root free to hold workspace-level configuration only. LICENSE is copied rather than moved: the package needs its own copy in the published tarball, and the repository still needs one at its root. Pure relocation, byte for byte -- every path is a 100% rename and no file's content changes. The root workspace configuration that makes this directory a pnpm workspace member lands in the following commit.
…ipeline pnpm-workspace.yaml gains the packages glob and linkWorkspacePackages, so a sibling added later is symlinked from the workspace rather than fetched from the registry, and keeps the install-time settings (build allowlist, release-age gate) that can only take effect at a workspace root. The root package.json is private and publishes nothing; it holds the task wrappers, the shared tooling devDependencies, and the git hooks. Tooling that is a property of the repository -- ESLint's root config, prettier, commitlint, lint-staged -- stays here, because only one copy of each can ever take effect; tooling that is genuinely per-package stays in the package. turbo.json grows the tasks each package's underscore-prefixed scripts already carried, ordered by ^_build so a sibling's dist/ exists before anything resolves an import through it. _typecheck, _lint and _test:smoke additionally depend on the package's own _build: the smoke suite and the JSON-schema generator both import ../dist/ by relative path. attw --pack becomes a task of its own rather than a bare CI step, depending on the build whose output it inspects. tsconfig.base.json holds the compiler options every package extends, so tightening a flag is one edit rather than one per package. The root tsconfig.json covers the root's own config files, which are otherwise in no program at all -- a file type-aware ESLint then refuses to lint. lint-staged runs eslint once per package directory, because ESLint's flat config resolves from the working directory: a root run over a staged file under packages/ would apply the root config, which ignores packages/**, and report the file as ignored rather than linting it. The root README describes the workspace and points at the package's own README for the library itself; AGENTS.md and CLAUDE.md symlink to it as they already do inside the package.
Each job ran its command against the repository root when that root was also the package; now it runs the same task across the workspace through turbo, so a package added later is covered with no edit here. Pull-request runs pass --affected, scoping every task to the packages the branch actually changed. The pnpm/Node/install/cache preamble each job repeated is extracted into a composite action, which also carries the turbo cache restore that makes one root job per task cheaper than re-running the work per package. attw --pack was a bare step in the Typecheck job, run after pnpm typecheck purely to reuse the dist/ that task had left behind; it is a turbo task now and `pnpm typecheck` reaches it, so the step goes. The release job runs the workspace orchestrator and reports what it released by diffing the remote's tags across the run, which is what the mirror and attestation jobs key off -- they check out that tag rather than main, since a queued release run can push further commits in between, and build and pack from packages/trilean. Dependabot gains the package manifests under packages/*, which a `directory: "/"` entry alone leaves unwatched, and a github-actions entry covering both the workflows and the composite action's own uses: steps -- including the ones holding id-token: write to sign release attestations.
semantic-release ran once for the whole repository, which was the same thing as the package while there was only one of it. It now runs through @exadev/semantic-release-workspace, once per package, with each package's commits path-filtered to its own directory and its tags in name@version form -- so a package's version follows its own changes and a change to one never bumps another. commitStrategy "single" makes one commit for the whole run rather than one per released package, which is why @semantic-release/git is absent from the plugin list: its own prepare step would produce exactly the per-package commit that mode replaces. Release notes move from the angular preset to conventionalcommits, so the changelog groups entries under a heading per commit type. That preset was unusable before -- it renders only against conventional-changelog-writer 9 or newer, which @semantic-release/release-notes-generator does not depend on -- and the pnpm override added with the workspace root supplies it, so the workaround the angular preset represented is no longer needed. commitTypes keeps its role as the single source of truth for which commit types exist: commitlint's type-enum, the analyser's release levels, and now each type's changelog heading all derive from the one list.
…ues from The per-package tag format is what carries a version across from before the workspace existed, and it differs from the one a single-package release used -- v1.3.0 then, trilean@1.3.0 now. Nothing in the config says so, and the consequence of not knowing is severe rather than cosmetic: with no tag in the new format, semantic-release finds no previous release, restarts the package at 1.0.0, and its first publish collides with a version the registry already holds.
…real output notify-hive still gated on needs.release.outputs.published, the flat single-package workflow's output name before the release job was restructured to emit released/version/tag. The condition silently evaluated to false on every run -- GitHub Actions treats a reference to a non-existent output as an empty string, which never equals 'true' -- so the job would never have fired.
Mearman
force-pushed
the
refactor/monorepo-restructure
branch
from
September 3, 2026 13:29
3974747 to
2f2fc4a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Moves the package into
packages/trilean/and makes the repository root a pnpm workspace with a turbo pipeline, so a second package can be added later without reshaping anything. The published package is unaffected: same name, same version sequence, same tarball.This is a structural change with no behavioural change intended. Nothing under
packages/trilean/src/was edited — the move commit is a 100% rename for every path.What changed
The package's own files (
src/,test/,scripts/, its manifest, tsconfigs, tsdown/vitest/wrangler configs, its ESLint config, README and CHANGELOG) moved underpackages/trilean/. The root now holds workspace-level configuration only: the pnpm workspace definition, the turbo pipeline, the shared tsconfig base, and the tooling configs.Releases move from a single
semantic-releaserun to@exadev/semantic-release-workspace, which releases each package from its own path-filtered commit history with tags inname@versionform. That tag format differs from the single-package one (v1.3.0then,trilean@1.3.0now), so atrilean@1.3.0tag was created at the same commitv1.3.0points at — without it semantic-release would find no previous release and restart the package at 1.0.0.CI is now one workspace-wide workflow driving every check through turbo from the root, with the repeated pnpm/Node/install/cache preamble extracted into a composite action. On pull requests, tasks run with
--affected; pushes to main run the whole workspace.Verification performed
I verified this from a completely fresh clone, independently of the implementation, and against a
mainbaseline checked out separately.Git history survived the move.
git log --follow --oneline -- packages/trilean/src/tree.tsreturns 6 commits — the move plus the 5 that predate it — which is exactly the countgit log --oneline origin/main -- src/tree.tsreturns for the same file before the move.git show --numstat --find-renames=100%on the move commit shows every path as a pure rename with zero added or deleted lines; the only non-rename entry isLICENSE, deliberately copied so the published tarball and the repository root each have one, and byte-identical to main's.Zero behavioural drift. Rather than diffing file by file, I compared git tree object hashes at branch HEAD:
packages/trilean/src,packages/trilean/testandpackages/trilean/scriptsare the identical tree objects as main'ssrc,testandscripts. That is a stronger result than a clean diff — it also proves no commit after the move touched them, which is the scope-creep check.Ran the full matrix from scratch on both branches. Counts match exactly:
pnpm install --frozen-lockfile,pnpm build,pnpm lint,pnpm typecheck(includingattw --pack, no problems found) all pass. The working tree is clean afterwards, soeslint --fixchanged nothing.The published artifact is provably unchanged. I packed both branches and compared the tarballs: identical file lists (58 entries), and every published file byte-identical. Comparing the 19 consumer-facing
package.jsonfields —name,version,dependencies,peerDependencies,engines,exports,files,publishConfigand the rest — gives zero differences. The only manifest changes arerepository.directory,homepage, workspace tooling moved out ofdevDependencies, and theattwscript, none of which reach a consumer.Version continuity.
packages/trilean/package.jsonis still"name": "trilean"(bare, unscoped) at"version": "1.3.0", withpublishConfig.access: public; npm'slatestis 1.3.0. I confirmedtrilean@1.3.0exists on the remote and points at 460864c, the same commit asv1.3.0. I then simulated the post-merge state — a bare remote whosemainis this branch's tip, carrying the real tags, cloned fresh — and ran the release orchestrator's dry run against it. It reportsFound git tag trilean@1.3.0 associated with version 1.3.0 on branch main, thenThe next release version is 1.3.1. It continues from 1.3.0 rather than restarting.CI is complete and valid. All three workflow files parse as valid YAML. The job set is identical to main's — same 11 keys and same display names: Commitlint, Lint, Typecheck, Test, Integration test, Smoke test, Workers runtime test, Release, Publish mirror to GitHub Packages, Attest SBOM and build provenance (npm), Attest SBOM and build provenance (GitHub Packages). Nothing was dropped. The turbo cache step moved into the composite action rather than disappearing, and dependabot was extended to cover
packages/*(plus a newgithub-actionsecosystem entry, which main lacked).The isomorphism guard still fires.
packages/trilean/eslint.config.tsis byte-identical to main's root config, so the Node-import bans, barrel policy and per-file exemptions all survive. I checked this functionally rather than by reading: injectingimport { readFileSync } from "node:fs"intosrc/json-value.tsproduces'node:fs' import is restricted ... This is an isomorphic library. The tsconfig split also reconstructs main's options exactly — 13 shared intsconfig.base.jsonplus the package's ownlib/types/resolveJsonModulegive the same 16 main had, andtsconfig.node.jsonis byte-identical.Worth knowing before merging
--affectedon pull requests is a genuine behavioural change to CI, not just a reorganisation: a PR touching only root-level files may not run the package's test jobs. Pushes to main run the whole workspace and the release job gates on all of them, so nothing untested can publish — but main could go briefly red for something a PR run skipped.The
trilean@1.3.0tag is load-bearing and lives outside the repository. It exists and is correct today; if it were ever deleted, the next release would restart at 1.0.0 and collide with a published version. The reasoning is recorded inrelease-workspace.config.tsrather than only in a commit message.I have deliberately not merged this. Restructuring a live, published, depended-on package warrants human sign-off even though every check above is clean.