Builds compile committed OpenAPI snapshots into compact versioned contracts
under ignored dist/contracts/. Catalog entries record the exact committed
hash; snapshots with explicit local annotations also record the upstream hash
and modification name. Generated contracts are packaged on npm but are not
committed.
# Build the Bun CLI and all versioned contracts
bun run build
# Add or refresh an immutable upstream snapshot
bun run conformance:sync -- --version <version>
# Add a stable release snapshot, update metadata, and verify it
bun run conformance:add-version -- v4.11.0The version-pinned black-box suite lives in conformance/.
It invokes every operation through the real CLI across historical Langfuse
specs. Active operations make one minimally valid mocked API call; operations
marked deprecated: true must fail before any network request.
bun test
bun run conformance:allbun test verifies the generator, schemas, serialization, capture oracle,
deprecation policy, and legacy CLI compatibility. bun run conformance:all
builds the package and checks every operation through the native CLI using its
lossless JSON input path. CI runs both.
The user-facing command surface (resource, action, aliases, deprecation) of
every snapshot is pinned in reviewed goldens under conformance/goldens/.
Tests and the build fail when compiled names differ from the goldens, so a
change to the naming heuristics cannot silently rename commands. After an
intentional naming change, regenerate and review the diff:
bun run goldens:updateAll option flags are derived mechanically: query/header parameters and
request-body fields kebab-case their wire names (objectId -> --object-id),
so new spec versions get flags without per-parameter maintenance. The
compiler validates each operation's full flag namespace (parameters, aliases,
body fields, reserved and global flags) and fails the build when a new
snapshot introduces a collision, instead of silently shipping a dead or
hijacked flag.
Hand-written naming exceptions live in src/contracts/overrides.json and are
applied by the contract compiler, never hardcoded in the runtime: extra
parameter flag spellings (parameterFlagAliases, e.g. --prompt-version),
body-field flag renames for collisions (bodyFieldFlags, e.g.
llmConnections_upsert.secretKey -> --provider-secret-key because
--secret-key is the global auth flag), and per-version command overrides.
A snapshot that lacks the referenced parameter or field skips the entry, but
an entry applied in no snapshot at all fails the build, tests, and
goldens:update, so stale overrides cannot rot silently.
Push to main freely; nothing publishes on push. Releases are a three-step
flow with a human gate in the middle:
bun run release- Cut (local, interactive): verifies you are on
main, in sync with origin, andghis authenticated; checks CI status on HEAD (warns and asks when it cannot be confirmed green — note that a commit whose checks have not started yet passes this silently, so wait for CI after pushing); asks for the next version (patch/minor/major, or alpha/beta/rc prereleases — other identifiers and build metadata are rejected, matching the publish workflow's policy); verifies the version is unused under both npm package names and the tag is free; runs typecheck, both test suites, and the full conformance build; then pushes achore(release): vX.Y.Zcommit plus thevX.Y.Ztag and opens a draft GitHub release with generated notes. - Publish the GitHub release: edit the notes on GitHub and click Publish. This is the release decision — nothing reaches npm before it.
- npm publish (automatic): publishing the release triggers
release.yml, which re-verifies the release against the same policy module the cut script uses (scripts/release-guard.ts: tag == package.json version, commit on main, prerelease consistency, identifier whitelist, both package channels in sync, and dist-tags never moving backwards), verifies both packed tarballs, re-runs all gates, and publishes@langfuse/clifollowed by thelangfuse-clicompatibility package via npm trusted publishing (OIDC) with provenance attestations. No npm token exists anywhere.
Both packages come from one build. scripts/npm-packages.ts stages two package
roots with identical bin/, dist/, version, and langfuse executable. The
canonical package receives README.md; the compatibility package receives
npm/legacy/README.md as its npm README.md. The scoped package publishes
first. If the second publish fails, rerun the workflow: the guard skips an
existing version only when its gitHead matches the release commit, then
publishes the missing package.
npm dist-tags derive from the version: stable → latest, -alpha.N →
alpha, -beta.N → beta, -rc.N → rc. Prerelease versions must be
marked "pre-release" on the GitHub release (the draft is created that way);
the workflow fails closed on any mismatch. Graduating 1.1.0-rc.1 → 1.1.0
is just another bun run release run choosing "graduate".
To test the cut flow without pushing anything:
bun run release -- --dry-runIf testing local changes to the release script itself, add --allow-dirty.
Never use --allow-dirty for a real release.
Only when Actions is unavailable, publish directly from a machine:
bun run release -- --publish-localThis runs the same gates plus npm pack --dry-run for both staged packages and
an explicit publish confirmation, and requires interactive npm authentication
(with OTP if either package disallows tokens). It publishes the canonical
package first and the compatibility package second. It does not commit or tag;
do that manually after.
--tag <dist-tag> overrides the dist-tag in this mode only; the CI path
always derives it from the version.
The public @langfuse/cli package must exist before npm permits trusted-
publisher configuration. Bootstrap it once as a public prerelease from an
authenticated @langfuse organization owner, using 2FA and
npm publish --access public. Do not use the stable version intended for the
first automated dual publish.
On npmjs.com → @langfuse/cli → Settings, and again on
langfuse-cli → Settings:
- Trusted Publisher → GitHub: owner
langfuse, repositorylangfuse-cli, workflow filenamerelease.yml, environmentnpm-publish. - Publishing access: "Require two-factor authentication and disallow
tokens" — CI publishes via the trusted publisher, humans can still
--publish-localinteractively with OTP, and no token can ever publish.
In the GitHub repo, environment protection is required, not optional:
release events execute the workflow file as of the tagged commit, so
without protection anyone with push access could tag a commit carrying a
modified release.yml and publish arbitrary code through the trusted
publisher. The environment protection rules are the platform-level gate that
closes this:
- Settings → Environments → create
npm-publish. - Add required reviewers (release approvers).
- Set deployment branches and tags to "Selected branches and tags" and
allow only tags matching
v*. - Additionally, add a repository ruleset restricting who can create
v*tags (Settings → Rules → Rulesets) to maintainers.
The compatibility tarball is functional but its npm README directs users to
@langfuse/cli. After both packages for a version are verified, an
authenticated npm owner may also attach npm's install-time notice:
npm deprecate 'langfuse-cli@<version>' \
'Renamed to @langfuse/cli. v2 will only be published there. Migration: https://github.com/langfuse/langfuse-cli#readme'The workflow does not run npm deprecate: trusted publishing authenticates
package publication, while registry metadata changes remain an explicit owner
operation. At v2, remove the compatibility staging/publish steps, publish only
@langfuse/cli, and deprecate the complete legacy version range. Never
unpublish the old artifacts.