Skip to content
stellaPublic

About

Shared configuration for stella packages

Topics

Resources

Code of conduct

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Stella tooling

stella tooling

Shared TypeScript, oxlint, and Rust configuration for stella public packages.

This repo intentionally contains only portable tooling policy:

  • @stll/typescript-config: strict TypeScript config presets.
  • @stll/oxlint-config: general upstream oxlint rules and the shared stella-lowercase and no-raw-colors JS plugins.
  • @stll/oxlint-plugin: portable static-safety rules for unsafe type assertions, incomplete union-keyed records, raw DOM HTML sinks, and route-query waterfalls.
  • @stll/time: named duration constants, calendar-date parsing and arithmetic, the calendar day in a time zone, and a side-effect-free Temporal.
  • @stll/test-kit: hermetic test primitives for Bun: a controllable clock, event-based waits, per-test temp directories and an outbound network guard.
  • rust/: source-of-truth Rust formatting, lint, and Cargo profile templates.
  • rust-lints/: Dylint libraries for stella-specific Rust rules.

Repo-specific stella rules stay in the consuming repo: domain authorization, i18n, generated native artifacts, benchmark exceptions, and package-specific ignores. Route-query conventions are shared here because they are the common TanStack Router + React Query contract.

Shared toolchain

@stll/oxlint-config/toolchain.json is the versioned source for shared tool versions. Install @stll/oxlint-config as a dev dependency, inherit its pins, and run bunx --no-install stll-toolchain-check from the repository root in CI. The CLI inspects tracked configuration files, prints path:line: [rule] message, and exits with status 1 on a mismatch. Tools absent from a repository do not introduce requirements.

Schema version 1 contains these fields:

Fields Contract
schemaVersion Supported policy schema version (1).
bun Minimum Bun; the repository pins one exact release at or above it everywhere.
oxlint, oxlint-tsgolint, @oxlint/plugins, @stll/oxlint-plugin, lefthook Exact dependency and catalog pins. Declared local workspace packages must resolve to the same version.
typescript, typescriptInstallLayouts, typescript6Compatibility Exact compiler pins and complete direct or split install layouts; the optional compatibility alias is exact too.
node, python, rust Runtime selectors and version-manager pins. Node selects the policy major (26.x); static minor and patch selectors within that major are allowed. A Python minor pin preserves existing patch selectors within that release series; a patch policy requires the exact patch. Rust selects one exact stable release across toolchain files, commands, workflow selectors and images.
actions Approved action names mapped to full sha and version comment.
dependabot Shared schedule, cooldown and groups. Package and action ignores derive from the owned pins. actionGroups come first in every GitHub Actions entry, so first-party reusable workflows that move by commit arrive as one update.
formatter bun fmt is the only formatter. style is the one [fmt] style (line width, import groups) every Bun repository's root bunfig.toml repeats exactly; ignorePatterns lists the generated files every repository leaves to their generators. A repository adds only its own ignore patterns and, where it uses Tailwind, sortTailwindcss.

The formatter-policy rule makes bun fmt the only formatter. It fails on an oxfmt, Prettier, Biome or dprint dependency, catalog entry, script or command anywhere in the repository; on .oxfmtrc*, oxfmt.config.*, .prettierrc*, prettier.config.*, .prettierignore, biome.json and dprint.json files and on a prettier key in package.json; on a package script named fmt (it would shadow bun fmt); and on a changesets format other than false. In a Bun repository the root bunfig.toml holds the only formatter configuration in its [fmt] table: exactly the shared style from toolchain.json, the shared ignore patterns, and nothing repository-specific beyond extra ignores and sortTailwindcss; lefthook pre-commit runs bun fmt --staged, and a workflow runs bun fmt --check without path operands, so CI always checks the whole repository.

Configure oven-sh/setup-bun with bun-version-file: package.json and a matching packageManager. Configure setup-node/setup-python with node-version-file/python-version-file referencing tracked .node-version or .nvmrc / .python-version files; direct version inputs are forbidden. Node and Python image tags and mise/asdf entries inherit the same pins. The Node policy is 26.x: selectors must select major 26. Bare major, major wildcard, minor and stable patch selectors within that major are allowed; open ranges, other majors, prereleases and floating aliases fail. Release and publish workflows use exact patch version-file selectors for reproducible builds; this repository pins .node-version to 26.10.0. The series policy accepts that patch without weakening release validation. Runtime images may retain a full SHA256 digest alongside an exact stable patch tag: Node must be inside its policy series and match tracked root .node-version when present; Python must be a patch in its policy series; Bun must equal its policy version. Supported image variants remain allowed. Static image families outside Node, Python, and Bun are outside this runtime check; unresolved image names still fail. Digest validation is declaration-only: the guard enforces the exact policy tag and a full SHA256 digest, but does not verify that the digest belongs to the tag. Dependabot refreshes digests per repository; a shared tag-to-digest map would require a tooling release for every base-image digest bump across repositories, which costs more than it buys. GitHub JavaScript action host runtimes (runs.using) are platform-managed and do not select the project toolchain Node version.

A published package can test the minimum Node major in its engines.node support range in one declared workflow job. Add a scoped engineFloors entry to tracked root stll-toolchain.json:

{
  "engineFloors": [
    {
      "package": "packages/library",
      "workflow": ".github/workflows/ci.yml",
      "job": "node-floor"
    }
  ]
}

That job may use a literal exact stable patch in setup-node's node-version whose major matches the range's minimum major and whose release satisfies the range (for example, 20.10.0 for >=20.10.0). The approved action SHA, version comment and input validation also apply. Other jobs and runtime files follow the shared Node series. Missing packages or jobs, private packages, absent setup-node selectors and floor mismatches fail configuration validation. optOuts is optional when only engineFloors is declared.

The Rust stable pin is 1.99.0. Floating channels and off-pin Rust references fail the shared check. Cargo rust-version declarations remain minimum support versions and may be lower than the shared pin. Rust setup actions (dtolnay/rust-toolchain and actions-rust-lang/setup-rust-toolchain) require explicit exact toolchain inputs; every release in a comma-separated setup list must satisfy the pin or its declared exception.

Rust exceptions use one closed declaration in root stll-toolchain.json:

{
  "rustExceptions": [
    {
      "pin": "1.97.1",
      "reason": "Awaiting upstream support for the shared compiler",
      "tracking": "https://github.com/owner/repository/issues/123",
      "expires": "2026-11-11"
    }
  ]
}

Replace the example with a verified compatibility constraint and its tracking issue. Each exception permits only that exact release and must expire in the future. Floating channels, duplicate pins, extra fields and rust-version opt-outs fail validation. The same exception applies to toolchain files, workflow Rust inputs and environments, Cargo/rustup commands and Rust images.

Hawk 0.1.11 is the current temporary Rust exception: its driver fails to build with 1.99.0 due to compiler-private API changes (E0061 and E0308). It uses 1.97.1 until tracking issue #99 is resolved, with the exception expiring on 2026-11-11. The stable source checker and all ordinary Rust work use the shared 1.99.0 pin. Consumer repositories that run Hawk need the same exact, tracked exception declaration.

engines.node and requires-python are support ranges: they must include the shared runtime. The Node support range must include every stable release in the policy major, so >=26, ^26 and ^24 || ^26 pass; ^24, <26, >=27 and ranges that omit part of major 26 fail. A minor Python pin requires support for every patch in that series. Python comparisons, compatible releases, wildcard exclusions, and comma intersections are supported for final releases; unsupported syntax produces a diagnostic.

Direct compiler invocations in scripts.typecheck must use the selected layout command. Classification decodes shell tokens to find compiler calls; acceptance requires the declared command text after leading environment prefixes are removed, including its option spelling and quoting. Delegated wrappers and workspace runners are checked by the consumer typecheck parity contract rather than inferred from manifests.

Listed shared actions require their approved SHA and matching # vX comment. CI also checks the shared release policy's runtime selector action set against toolchain.json, deriving its immutable checkout ref from the existing release-policy workflow reference. Other remote actions and reusable workflows require a full SHA. Local actions are repository-owned. The checker resolves YAML aliases and TOML tool tables. The package guide is the canonical contract for checkout provenance, runtime version files and scoped dynamicSelectors. It documents current-source and delegated refs, reusable-workflow caller context, sparse-checkout requirements, declaration locators and fail-closed validation. Use scoped declarations for reviewed unresolved selectors rather than broader rule opt-outs; literal pins and local-action provenance remain enforced.

The named rules are bun-pins, package-pins, typescript-layout, node-engine, node-version, python-version, rust-version, runtime-manager, runtime-docker, runtime-workflow, action-pins, workflow-trust, dependabot-policy, and cloud-setup-drift. Non-Rust rule exceptions require a tracked root stll-toolchain.json listing the rule and its reason:

{
  "optOuts": [{ "rule": "bun-pins", "reason": "This repository uses npm." }]
}

Workspace TypeScript toolchains combine the root and declared members' devDependencies. An included member may declare the exact TypeScript 6 compatibility API across dependencies, devDependencies, and peerDependencies when that shared install uses the complete split layout. Ambiguous or partial layouts do not grant this allowance. Peer ranges declare support: they must include the policy compiler release and the compatibility release when the selected layout uses it. Ranges do not contribute to the installed compiler inventory.

JavaScript update roots include the repository root, declared workspace membership, and directories with their own bun.lock, package-lock.json, yarn.lock, or pnpm-lock.yaml. Independent manifests with dependencies, an explicit package manager, or dependency-bearing workspace members also create update roots. Metadata-only fixtures do not. Package-manager selection follows the nearest declaration or lock; workspace members share their owner's update entry. Bundler Gemfile/Gemfile.lock and Composer composer.json/composer.lock declare roots at their own directories.

rust-version, node-engine, workflow-trust and cloud-setup-drift are mandatory: opt-outs cannot bypass the shared Node support range or generated cloud setup requirements.

Unknown rules, empty reasons, duplicate rules, and malformed configurations fail. Opt-outs apply repository-wide to the named rule, so keep them narrow.

.github/dependabot.yml is the shared reference. It declares each detected package ecosystem and update root (Bun roots use bun, npm/pnpm/Yarn use npm, and uv projects use uv), uses the policy schedule/groups and a five-day cooldown, and ignores tooling-owned npm and action pins. Workspace members share their update root; independent manifests need their own entry. pnpm-workspace.yaml owns pnpm catalogs and membership. Omitted packages includes only the root, matching the current pnpm contract; declare member patterns explicitly to keep membership stable across pnpm versions. The generator derives the reference from the same policy and ecosystem registry as the guard: run bun run generate:dependabot after policy changes. Consumers run bunx --no-install stll-dependabot from their repository root; the command reads tracked declarations and writes canonical .github/dependabot.yml (or updates an existing .yaml). stll-toolchain-check rejects missing coverage and policy drift. Use stll-dependabot --check in CI for read-only byte drift detection. Pass --config stll-dependabot.json to declare ecosystems and directories explicitly; the declaration cannot change shared schedule, cooldown, groups, or owned-tool ignores. Repository holds on other dependencies go in stll-toolchain.json dependabotIgnores, each with a reason; they cannot name or match a shared pin. See the package guide for the input format. The published @stll/oxlint-config/dependabot subpath exports generateDependabotConfig and parseToolchainPolicy for programmatic generation. Owned package pins follow release metadata. The changeset:version command first includes the policy package in the release plan whenever an owned workspace pin changes, then synchronizes pins through bun scripts/check-lockfile-workspace-versions.ts --write, which also refreshes cached workspace versions without regenerating the dependency graph.

Package quarantine

Before installing dependencies, run the CLI from the exact published @stll/oxlint-config release that includes stll-quarantine. Replace X.Y.Z with that release's exact version; bunx installs the pinned package when it is not already available:

bunx --bun --package @stll/oxlint-config@X.Y.Z stll-quarantine --sync
bun install

stll-quarantine --sync creates or updates the Bun quarantine preset. The preset keeps the five-day release-age gate for third-party packages and writes permanent exemptions for exact registry-backed @stll/ package names from declarations and the lockfile, including native platform packages. Wildcards and version selectors are rejected. Creation timestamps record audit history; they never expire.

Third-party exceptions require an inline exact UTC expiry:

[install]
minimumReleaseAge = 432_000
minimumReleaseAgeExcludes = [
  "@stll/oxlint-config", # quarantine-excluded-since: 2026-10-17T00:00:00.000Z
  "third-party-package", # quarantine-expires: 2026-10-20T00:00:00.000Z
]

stll-toolchain-check validates tracked root bunfig.toml and warns starting 48 hours before a temporary exception expires. Expired exceptions warn during the existing 24-hour removal grace period, then fail validation. The standalone stll-quarantine --check command performs the same policy check without writes; --prune removes expired third-party entries and retains first-party entries. Only --sync changes the generated first-party set. Unrelated Bun settings and temporary third-party entries remain repository inputs.

The shared scheduled quarantine workflow runs the same tooling-owned core from an immutable source checkout, before installing consumer dependencies. It opens or refreshes the existing removal branch, so each repository has one removal PR. Consumers retain the pinned stella/.github policy and scheduled-prune callers; adopt the workflow revision that delegates to this core.

Usage

Install the shared TypeScript and oxlint packages:

bun add -d @stll/typescript-config @stll/oxlint-config @stll/oxlint-plugin @oxlint/plugins oxlint oxlint-tsgolint typescript

The shared defaults require TypeScript 7.0.2 or newer, oxlint 1.87.0 or newer, and oxlint-tsgolint 7.0.2003 or newer. Pin the current versions from @stll/oxlint-config/toolchain.json; do not use the deprecated oxlint-tsgolint 0.x line.

toolchain.json defines two supported TypeScript install layouts:

  • direct installs TypeScript 7 as typescript for compiler-API consumers and declaration generation. Prefer this when every framework supports TypeScript 7.
  • split-compatibility keeps TypeScript 6 as typescript for incompatible compiler-API consumers, then installs TypeScript 7 as @typescript/native for tools that explicitly require that compiler.

TypeScript 6 is compatibility-only. Use the split layout only for a command that loads one of the peer blockers listed in toolchain.json, or for code that imports the TypeScript compiler API. Remove the compatibility install when the blocker accepts TypeScript 7. The shared config's peer range accepts both layouts. Both layouts use bun check with the pinned Bun for typechecking; retain TypeScript wherever a tool needs its compiler API or declaration generation. stll-typecheck-parity runs from a consumer repository root. It uses that repo's TypeScript compiler (including the declared split layout), compares repository diagnostics by file, line, and code, then checks 31 shipped fixture classes under the consumer tsconfig flags. TypeScript diagnostics determine each class's activation; inactive classes are identified in the table, and zero active classes fail the check. The command fails on diagnostic differences or lost seeded coverage and reports wall time and peak RSS for TypeScript and Bun. It requires Bun and /usr/bin/time on Linux or macOS, and runs with Node or Bun. Tooling invokes the same bin through bun run check:typecheck-parity. The bin invokes bun check with its dedicated project/build flags. Consumer repositories must not define a check script: it shadows Bun's checker, so the parity bin fails with a clear error until that script is renamed. Standalone bun --check checks the current project but does not forward these checker options. Without flags, the command checks the root tsconfig.json and uses build mode for project references. To check selected configs, repeat --project, for example stll-typecheck-parity --project packages/api/tsconfig.json --project packages/web/tsconfig.json. Selected configs run independently without discovering or building their references; reference metadata is retained for ordinary project checks. No root tsconfig is required in this mode, and the compiler always comes from the repository-root installation. Missing, unreadable, and zero-input selected configs fail the check. --help prints usage and exits successfully. In default mode, referenced configs are discovered recursively and grouped by effective compiler options. Each group copies its representative config's effective options, removes emit-only and build settings, and sets noEmit for the seeded projects. Type-checking options and consumer type/module resolution are preserved. Each group must activate at least one seeded class. The installed compiler and Bun runtime must match their selected toolchain policy versions. Both compilers check the same temporary project graph. TypeScript declarations and build metadata stay in that temporary tree; consumer files remain untouched. The parity bin does not validate the consumer project graph; the repository's own typecheck does. Source errors normalize TypeScript's diagnostic exit 2 (including declaration-only inputs) to Bun's diagnostic exit 1; the report retains both raw exit codes. Other exit statuses compare exactly. Comparisons retain source diagnostic locations, normalize bundled standard-library paths, and distinguish locationless messages. Fixtures preserve the consumer package module context and reject configuration errors. Tagged agent diagnostics are normalized into the same comparison set.

Use the library TypeScript preset:

{
  "extends": "@stll/typescript-config/library.json",
  "include": ["src"]
}

The package also exports base.json, node.json, bun.json, published.json (JavaScript disabled, isolated declarations enabled), and react-library.json (DOM libraries and the React JSX runtime). These profiles inherit errors for unreachable code and unused labels.

With the TypeScript 7 native compiler, opt into one checker per project by adding @stll/typescript-config/single-checker.json after the main profile in an extends array. This additive profile changes only the checker count:

{
  "extends": [
    "@stll/typescript-config/react-library.json",
    "@stll/typescript-config/single-checker.json"
  ],
  "include": ["src"]
}

checkers: 1 also applies to native checker consumers that read tsconfig, such as oxlint-tsgolint. This profile requires the native compiler; TypeScript 6 does not recognize checkers. A repository-recorded application measurement compared one checker at 82 seconds / 3.0 GB / 12.0 million instantiations with four at 87 seconds / 5.3 GB / 31.3 million instantiations. These results are specific to that project, not a benchmark of every consumer.

Import-extension rewriting, symlink preservation, incremental compilation, and tsBuildInfoFile placement remain explicit consumer options: they depend on emit requirements, module identity, and cache ownership. The shared base sets none of them.

Node scripts need an explicit TypeScript project when the library project's include does not cover them. Install @types/node as a direct development dependency of the consumer, then add scripts/tsconfig.json:

{
  "extends": "@stll/typescript-config/node.json",
  "include": [
    "**/*.js",
    "**/*.jsx",
    "**/*.mjs",
    "**/*.cjs",
    "**/*.ts",
    "**/*.tsx",
    "**/*.mts",
    "**/*.cts"
  ]
}

The existing Node preset selects Node ambient types; the local include assigns ownership of scripts to that project. Keep the library project unchanged. For scripts in .github/tools, add .github/tools/tsconfig.json with the same extends and all local include patterns shown above. A sibling scripts/tsconfig.json that includes ../.github/tools does not own those files: Oxlint discovers the nearest tsconfig.json; run the ordinary type-aware lint command from the repository root. Missing Node ambient types can otherwise appear as TS2591 on node:fs or require, followed by misleading any diagnostics on Node API results. Adding casts or changing conditionals does not repair resolution. Use the existing bun.json preset for Bun tests and benchmarks. Include root configuration files in their declared project as well. No additional preset, compiler overrides, or config import side effects are needed.

stll-toolchain-check enforces this ownership with type-aware-projects when the root manifest declares oxlint-tsgolint. It compares the files selected by the root lint command with configured root files. The search starts at the nearest discoverable tsconfig.json and its tracked reference graph, then searches ancestor configs and their references for unmatched files. An unreferenced sibling cannot claim ownership. Imported files do not become configured roots merely because the compiler loads them into a program; the pinned backend resolver checks root membership. Oxlint's --tsconfig flag changes import resolution; it does not select a type-aware ownership project. Only tracked JavaScript and TypeScript files selected by lint are checked; lint exclusions stay excluded. An unrelated, unreferenced config does not establish ownership. Oxlint supplies its file inventory and the TypeScript compiler expands inherited config, JSONC, includes and excludes; this check does not run lint rules or type checking. Unsupported lint launchers or unreadable project configurations produce diagnostics rather than bypassing the check. Add the appropriate shared project declaration instead of suppressing the rule.

Script inventory includes automatic pre/post stages of literal package-manager run aliases and the root lint script. Declare the root packageManager when root hooks exist; workspace lint tasks inherit it. npm and Bun run those hooks; tracked project .npmrc ignore-scripts=true suppresses npm hooks. Pinned pnpm 12.9.1 defaults to running them and reads the boolean enablePrePostScripts from tracked pnpm-workspace.yaml; it skips a hook whose name already occurs in the main script body, matching that version's behavior. Yarn Berry (declared major 2, 3 or 4) does not run arbitrary pre/post hooks. Unknown manager flavors, unreadable configuration, nested workspace overrides, custom Yarn executables or plugins, and dynamic lifecycle settings produce diagnostics. Automatic hook bodies do not recursively invent further pre/post stages. Filtered Bun build preparation requires one exact workspace package name and no prebuild/postbuild hooks; unsupported hook scopes must be made explicit instead of being omitted from inventory.

Use the oxlint preset with local exceptions:

import { library } from "@stll/oxlint-config";

export default library({
  ignorePatterns: ["dist/", "npm/", "*.node"],
  overrides: [
    {
      files: ["scripts/**"],
      rules: {
        "no-console": "off",
      },
    },
  ],
});

Add the portable safety rules to an existing Oxlint config:

import { defineConfig } from "oxlint";

import {
  portableSafetyPluginSpecifiers,
  portableSafetyRules,
} from "@stll/oxlint-plugin";

export default defineConfig({
  jsPlugins: [...portableSafetyPluginSpecifiers],
  rules: { ...portableSafetyRules },
});

For TanStack Router + React Query applications, add the route-query guards to prevent cache misses and render-fetch waterfalls during navigation:

import { defineConfig } from "oxlint";

import {
  routeQueryPluginSpecifiers,
  routeQueryRules,
} from "@stll/oxlint-plugin";

export default defineConfig({
  jsPlugins: routeQueryPluginSpecifiers,
  rules: routeQueryRules,
});

require-loader-prefetch requires every statically attributable useSuspenseQuery in a route to be referenced by that route's loader. no-raw-route-query-client requires route freshness helpers instead of raw TanStack Query client calls and keeps pending components synchronous.

When using no-unsafe-inner-html, disable the blanket react/no-danger rule. Its default sanitizerPolicy: "allowlist" permits static HTML and calls whose names match the shared HTML sanitizer allowlist. Set sanitizerPolicy: "provenance" to require a // safe-html: <provenance> comment for dynamic values, including calls to sanitizer-named functions.

Oxlint 1.80 replaced react/react-compiler with category-specific React Compiler rules. Enable the actionable categories explicitly with library({ react: { files: ["src/**/*.{ts,tsx}"] } }); select only React-owned paths, including .ts hooks. The base library() preset is framework-neutral; react/invariant and react/todo remain off because they report compiler internals rather than source defects. Consumers can suppress one precise category without hiding unrelated compiler diagnostics. A repo adopting these rules against an existing findings backlog should carve out temporary overrides entries per React-owned legacy path and fix forward. Each consumer React override must include the React plugin: oxlint resolves its rules against its own plugin list or the framework-neutral base list, not an earlier override.

import { library, libraryPlugins } from "@stll/oxlint-config";

export default library({
  react: { files: ["src/**/*.{ts,tsx}"] },
  overrides: [
    {
      files: ["src/legacy/**/*.{ts,tsx}"],
      plugins: [...libraryPlugins, "react"],
      rules: { "react/hooks": "off" },
    },
  ],
});

Include any additional consumer plugins in that override's plugins list too.

Use the helper directly as the root config when possible. That keeps root-level options, shared JS plugins, shared ignores, and local exceptions in one merged object.

If a repo needs the extends style used by other oxlint config packages, keep repo-specific ignores in the root config:

import { defineConfig } from "oxlint";

import stella from "@stll/oxlint-config";

export default defineConfig({
  extends: [stella],
  ignorePatterns: ["dist/", "npm/", "*.node"],
});

CommonJS repos can use require in oxlint.config.ts:

const { library } = require("@stll/oxlint-config");

module.exports = library();

Recommended scripts:

{
  "scripts": {
    "typecheck": "bun check",
    "lint": "bun --bun oxlint -c oxlint.config.ts --report-unused-disable-directives-severity=error --deny-warnings --type-aware .",
    "lint:fix": "bun --bun oxlint -c oxlint.config.ts --type-aware --fix ."
  }
}

Use the Rust templates by copying them into a Rust repository:

cp rust/rustfmt.toml /path/to/repo/rustfmt.toml
cp rust/clippy.toml /path/to/repo/clippy.toml
cp rust/cargo-config.toml /path/to/repo/.cargo/config.toml

Then copy either rust/cargo-root.toml or rust/cargo-workspace.toml into the repository's root Cargo.toml. Cargo does not support extending these settings from another package, so the templates are kept here as the canonical source and synced into consumers.

Install the stable source lint using an exact tooling commit, then run it after Clippy. Replace <sha> with the commit containing the adopted checker:

cargo install --git https://github.com/stella/tooling --rev <sha> --locked stella_source_lint
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
cargo ci-stella-lint

Cloud setup

This repository adopts its own shared cloud setup with pinned Bun, Node, and Rust (Clippy and rustfmt). Provision a cloud environment with bash .agents/cloud-setup.sh install; run bash .agents/cloud-setup.sh start at each session start (the committed Claude SessionStart hook does this).

Releasing packages

Add a Changeset for every pull request that changes a published package. Choose the affected package and semantic bump with bun run changeset; use an empty Changeset when a release is intentionally unnecessary.

After the change lands on main, the shared release workflow maintains one Version Packages pull request. Merging that pull request updates package versions and changelogs, then .github/workflows/publish.yml builds the tarballs and delegates the hardened npm and GitHub release transaction to the versioned stella/.github contract. Package tags use the immutable <name>@<version> form.

Shared cloud setup

Declare cloud in tracked root stll-toolchain.json, then run bunx --no-install stll-cloud-setup to generate .agents/cloud-setup.sh and merge its remote-only Claude SessionStart hook into .claude/settings.json. Generation recreates its deleted tracked output files and requires unrelated tracked inputs to remain readable. The declaration requires install exactly bun install --frozen-lockfile or bun install --frozen-lockfile --ignore-scripts. Use the second when lifecycle scripts (for example postinstall code generation) are too heavy for cloud setup; tasks then run those scripts themselves when they need the output. Every setup phase logs its start and duration to stderr. services is any subset of postgres and valkey; omit it or use [] for runtimes and dependencies only. A canonical repository-relative envFile, such as .env.cloud, is required with services and rejected without services. Runtime-only scripts emit no environment file code. A tracked root .node-version must select an exact stable patch within the shared Node series. Root rust-toolchain.toml enables pinned Rust automatically; cloud.rust can also declare its toolchain, components, and targets. To provision another authorized compiler, explicitly select cloud.rust.additionalToolchains: ["1.97.1"]; each extra must match the shared pin or a valid rustExceptions entry. Extras use the minimal Rust profile. Exceptions alone authorize versions and install nothing; additional compilers do not install Hawk or its required components. Optional cloud.tools installs exact supported releases of cargo-nextest, wasm-bindgen-cli, and uv from verified prebuilt artifacts. The wasm-bindgen CLI pin must match the crate in Cargo.lock. Installed commands work in a fresh shell without sourcing runtime environment files. The mandatory cloud-setup-drift rule compares the committed script with the same generator and rejects missing, changed, or undeclared scripts and missing or conflicting startup hooks.

The script has explicit install and start commands. It targets Ubuntu 24.04 with root access or passwordless sudo; these are capability requirements, not assumptions about every cloud image. See the package guide for lifecycle wiring, service isolation, environment-file ownership, and host documentation.

Published package contracts

Run stll-publish-contract in PR CI. Every published root or declared workspace package commits publish-contract.json, recording engines, peer support ranges, resolved JavaScript targets, and entry points. stll-publish-contract --write records an explicit contract decision as a reviewable diff; it also validates consumer support and cannot accept development-only requirements.

The consumer policy pins Node 22.23.3, npm 12.2.0, pnpm 12.9.1, and TypeScript 6.0.3 independently of the development toolchain. Published engines and TypeScript peers must support the consumer versions; ranges may include newer versions. An absent engines.node is unrestricted, matching package-manager semantics; a declared range must contain the consumer Node version. Both guards share this rule. Bun runtime/compiler requirements are rejected. JSON and declaration-only packages record {"type":"types-only"} explicitly.

Build adapters use the installed configuration loaders and bind their behavior to tsdown 0.23.1, Vite 8.3.4, or @nuxt/module-builder 1.0.3 (unbuild 3.6.1 and mkdist 2.4.1). They record final transform targets, including separate Nuxt module and runtime entries. No syntax lowering records esnext. Only exact single build invocations (tsdown, vite build, or nuxt-module-build build) are supported. Shell composition, environment prefixes, launchers, directory changes, workspace filters, CLI overrides, and pre/post build lifecycle scripts fail. Vite accepts its version-bound default plugin pipeline and the default @vitejs/plugin-vue 6.0.8 factory; custom Vue compiler, template, script, and feature options are unsupported. Nuxt accepts the version-bound builder and owned target hook. Unapproved plugins, output transforms, and execution-order changes fail. The static guard validates the supported configuration, without parsing the emitted JavaScript syntax. Add a reviewed adapter when adopting another build tool.

Nuxt modules can set both emitted targets without replacing the builder's entries:

import { nuxtModuleTarget } from "@stll/oxlint-config/build-target";

export default { hooks: { "build:before": nuxtModuleTarget("es2022") } };

The Nuxt adapter accepts this owned hook from the same package version as the CLI; it captures the normalized configuration before cleanup or output writes. The built publish-contract CLI is also checked against a TypeScript configuration and relative TypeScript import on the package's minimum supported Node release.

The static guard follows the packer declared in the tracked release workflow. With pnpm, it models publishConfig overrides for exports, main, module, types, typings, bin, and typesVersions. npm does not apply these overrides, so differing values fail; move the published entry into the source manifest. access and registry are allowed publication metadata. Other overrides, including engines, are rejected. Real npm and pnpm pack tests compare the modeled contract with each tarball manifest.

Packed-artifact checks run nightly, while the static contract check runs per PR. The reusable consumer job declares its exact scope in stll-toolchain.json:

{
  "consumerChecks": [
    {
      "workflow": ".github/workflows/consumer-compat.yml",
      "job": "consumer",
      "packages": ["packages/library"],
      "toolingVersion": "<installed tooling version>",
      "fixturePath": "tests/consumer"
    }
  ]
}

The declared workflow requires valid five-field cron with bounded numeric values, month/day names, lists, ranges, and positive steps; manual dispatch is optional. Day of week is 0–6 (SUN–SAT), matching the GitHub Actions schedule contract; numeric 7 is unsupported. The combined schedules must cover every weekday with unrestricted month and date fields, so weekly or seasonal schedules cannot satisfy the daily contract. Public packages require a semver-valid version. Published workspace packages under a vendor directory are unsupported; the contract CLI reports them rather than omitting them. bundleDependencies and bundledDependencies are unsupported because artifact staging does not install a bundled dependency tree; remove these fields before declaring consumer checks. The reusable consumer job must be stand-alone and unconditional (no needs, if, or strategy), so a job condition cannot skip its scheduled execution. That job calls package-consumer-compat.yml at the approved shared policy SHA with packages as the same JSON array and consumer-node equal to the consumer policy pin. Required tooling-version and fixture-path inputs equal the declared exact toolingVersion and repository-relative fixturePath; its consumer-compat.json must be tracked, parse with the runner’s fixture schema, and select exactly the declared package set. Replace <installed tooling version> with the exact installed @stll/oxlint-config release that provides the consumer runner; the guard binds the declaration to that package version. Every named package and its transitive workspace closure must be tracked, published, and have valid manifests. Each selected package must support that Node version. Unknown jobs, stale entries, undeclared consumer calls, and inconsistent inputs fail. This declaration applies to consumer checks; engineFloors retains its separate minimum-supported-major contract.

stll-consumer-compat --packages '["packages/library"]' --consumer-node 22.23.3 --fixture-path tests/consumer tests final tarballs in isolated projects with both npm and pnpm. The repository builds once with its development toolchain first. The runner reads the exact packer version and pack flags from a single tracked release workflow and packs directly with that manager. Missing, ambiguous, or unsupported pack configuration fails. Both consumers install the same release artifacts; the runner does not rewrite workspace or catalog protocols through a different manager. Fixture installs disable scripts, reject reserved runtime/manager bins throughout nested lifecycle paths, then approve only directly declared dependencies at their resolved lock identities. npm rebuilds those identities; pnpm runs its built-in pending rebuild. Both use the pinned consumer runtime before typecheck, build and smoke. Undeclared transitive build scripts receive no approval. Catalog entries that resolve to a local workspace package are unsupported: use workspace: for that dependency. Registry catalog declarations are staged intact; the release packer must resolve publication protocols. pnpm workspace owners require an adjacent tracked package.json; manifestless owners are unsupported. Public package names must be canonical lowercase, URL-safe npm names (at most 214 characters), with no traversal components. Workspace alias specifiers are unsupported: use the package name as the dependency key. The runner verifies the official Node archive checksum and exact runtime/package-manager versions, uses the oldest published React satisfying the package peer range and, when ReactDOM is required, its compatible renderer peer range, typechecks with the consumer TypeScript, and runs each fixture's build and usage smoke. Both publication packers and fixture commands use the provisioned consumer Node with isolated package-manager settings. Consumer command PATH contains only pinned wrappers, fixture binaries, and /usr/bin:/bin; it never inherits development tool directories. Installed binaries named node, npm, npx, or pnpm are rejected before fixture build and smoke commands.

The fixture directory contains consumer-compat.json:

{
  "packages": [
    {
      "package": "packages/library",
      "fixture": "library",
      "kind": "node",
      "build": ["npm", "run", "build"],
      "smoke": ["npm", "run", "smoke"]
    }
  ]
}

The selected package set must exactly match the fixture declaration set. Each fixture is a standalone project with its own source, package manifest, and TypeScript configuration. The package peers determine its fixture kind: a React peer requires react and a render smoke; other packages require node and an import with a representative call. A mismatching declaration fails. Dependency bindings, runtime tools, and caches belong to the runner. A nightly failure must open or update one issue in the consuming repository and use its existing failure notification.

Toolchain changes and typecheck probes

stll-toolchain-changed --since <git-ref> compares committed snapshots at the reference and HEAD. Fetch the reference and its history before running it. It prints one JSON object with changed, tools, and status; the detector exits successfully even when history is unreadable, reporting changed: true and status: "unreadable". Use changed to force the full CI depth on both pull requests and merge-queue runs. An unreadable snapshot never skips checks.

Run repository-installed tools only after the workflow's existing trust decision. The mandatory workflow-trust rule applies to PR workflows declaring a trust-check job. Repository execution requires a direct dependency and a condition requiring its trusted == 'true' output for every PR trigger. Before that boundary, commands must match a small metadata allowlist: shell output and string comparisons, bounded Git rev-parse/merge-base queries, and GET-only GitHub API reads. File readers, directory listings and file predicates require the job trust gate, including workspace and symlink paths. The trust job's if/elif comparisons and writes to GITHUB_OUTPUT use the same checks. Job containers and nonempty services require the job-level trust gate because they start before steps and supply executable code. A step condition cannot authorize their startup. Digest pinning alone does not approve an image for pre-trust execution; the guard has no implicit image approval catalog. Before trust, step actions must use a full SHA and match the explicit bootstrap catalog: actions/checkout and oven-sh/setup-bun, with bounded selector inputs. Bun installation requires an explicit literal version; version-file and path inputs require the job trust gate, including repository paths that may be symlinks. Checkout path and submodule traversal inputs also require trust. actions/setup-node requires trust because it also invokes package-manager commands from the checkout directory. Other actions and unsupported inputs require the job-level gate; a step condition cannot authorize them. Every job-level reusable workflow call requires the job-level trust condition, including pinned calls without inputs.

Interpreters, sourced files, executable paths, package managers (including metadata and script-disabled install modes), launchers, custom shells, and unknown commands or syntax require trust. Git history/diff commands and working-tree, index and fetch operations require trust; they are outside the revision-query allowlist. Unquoted pathname expansion also requires trust. Unsupported options and query programs also require the gate. For example, gate the entire detector job, including dependency installation:

check-changes:
  needs: [trust-check]
  if: >-
    always() && !cancelled()
    && ((github.event_name != 'pull_request'
         && github.event_name != 'pull_request_target')
        || needs.trust-check.outputs.trusted == 'true')
  runs-on: ubuntu-latest
  steps:
    # Check out, set up Bun, install dependencies, then run the shared detector.
    - run: bunx --no-install stll-toolchain-changed --since "$BASE_REF"

Actions interpolation is checked before shell quoting or comments. Only the bounded identity expressions github.sha, github.run_id, github.run_attempt and github.repository may appear directly in pre-trust scripts. Other contexts, functions and matrix values require environment indirection or the trust gate; the guard does not infer that an expression is safe from its shell quotes. Pass contributor-controlled metadata through a supported data environment key (PR_TITLE, HEAD_REF, ISSUE_BODY or COMMENT_BODY) and quote its expansion:

- env:
    PR_TITLE: ${{ github.event.pull_request.title }}
  run: echo "$PR_TITLE"

The trust job must classify each PR trigger used by the workflow. Main/manual jobs remain runnable when the PR-only trust job is skipped. Job or step conditions may prove that execution is limited to non-PR events; event string comparisons follow Actions case-insensitive equality. The guard proves only supported static metadata commands and boolean conditions; unknown commands and conditions cannot authorize execution. It does not validate the trust job's authorization policy. Workflows with no declared trust job retain their existing fork-CI policy.

The comparison includes Bun declarations, Node selectors and support ranges, resolved TypeScript compiler versions (including workspace and compatibility aliases), oxlint, oxlint-tsgolint, and shared tooling pins. Lockfile resolutions determine installed package versions; manifest ranges alone do not. Resolution locations and importer bindings are retained: swapping compiler versions between workspaces cannot look unchanged. Compiler specifier changes run parity even when the resolved release version is unchanged. Unknown lock shapes and ambiguous resolutions report an unreadable snapshot. Supported text locks include Bun JSONC (schema 0/1), pnpm YAML (5.4/6/9), npm package-lock/ shrinkwrap (1/2/3), and bounded Yarn Classic/Berry shapes; a binary-only Bun lock cannot be compared without executing Bun. Workflow/action and Docker definition edits conservatively force full CI. Unrelated application dependency changes do not force compiler parity. Setup-action runtime selectors and their tracked version-file contents must identify an exact stable release. Floating selectors, ranges, expressions, empty values, or omitted runtime versions report an unreadable snapshot and run parity even when their committed text is unchanged.

On pull requests, run the repository's ordinary Bun typecheck and select parity with the same detector; run the probe on demand:

stll-typecheck-probe --project packages/api/tsconfig.json --seed-dir packages/api/src -- bun run typecheck
stll-typecheck-parity --changed-since origin/main --project packages/api/tsconfig.json

The probe writes an exclusive temporary TypeScript file in the declared project, requires the command to fail with TS2322 attributed to that file, removes it, and requires the clean command to pass. Use --seed-dir to select a repository-relative directory inside the project; the default is the config directory. The config must include the seed directory; excluded seeds fail instead of producing a vacuous pass. Commands are argv, not shell expressions. Cleanup also runs on failure and handled termination. Repeat the probe for independently checked projects as needed.

Conditional parity runs when Bun, TypeScript, or shared declarations change; otherwise it prints parity skipped: toolchain unchanged since .... An unreadable reference runs full parity. Omit --changed-since to run full parity on demand. This repository runs conditional parity in CI, so a change to the compiler or toolchain is proven on the pull request that makes it. The probe, timed TypeScript checks and full parity run only through .github/workflows/typecheck-proofs.yml (workflow_dispatch), whose required changed_since input names the base of the change to prove: the detector compares the toolchain at that base with the head, so a default such as the previous commit could miss a change made earlier on the branch. It has no automatic or scheduled trigger. Node and lint-tool changes still force full CI through the shared detector.

Workflow, composite-action, Docker, mise, and tool-version declarations are conservatively fingerprinted for compiler parity, including referenced tracked setup-action version files (Node, Bun, Python, Go, Java and other runtimes). Rust toolchain-file selectors and conventional runtime files are included; cache-dependency-path is not a runtime selector. Compose and Kubernetes image definitions are conservatively fingerprinted alongside Dockerfiles. Catalog and unresolved declaration changes also force parity; an unclassifiable reference runs full parity. This may run parity for unrelated edits to these declaration files. Active installed compiler comparisons still use the owning lockfile resolutions.

About

Shared configuration for stella packages

Topics

Resources

Code of conduct

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages