Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
109 changes: 91 additions & 18 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
# Publishes to npm when a GitHub release is published.
# Publishes the canonical and compatibility npm packages when a GitHub release
# is published.
#
# Flow: `bun run release` pushes a release commit + tag and opens a draft
# GitHub release. A human edits the notes and publishes the release, which
Expand Down Expand Up @@ -52,6 +53,9 @@ jobs:
echo "::error::released commit is not on main"
exit 1
fi
RELEASE_SHA="$(git rev-parse HEAD)"
echo "RELEASE_SHA=$RELEASE_SHA" >> "$GITHUB_ENV"
echo "Release commit: $RELEASE_SHA"

- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
Expand All @@ -63,18 +67,20 @@ jobs:
- name: Install dependencies
run: bun ci

# Shared policy with scripts/release.ts (release-config.ts): validates
# Shared policy with scripts/release.ts validates both package identities,
# tag <-> version binding, prerelease consistency, identifier whitelist,
# and latest-tag monotonicity; emits the npm dist-tag.
- name: Verify release policy and derive npm dist-tag
# channel monotonicity, and safe partial-publish recovery.
- name: Verify release policy and derive dual-publish plan
env:
TAG_NAME: ${{ github.event.release.tag_name }}
IS_PRERELEASE: ${{ github.event.release.prerelease }}
run: |
set -euo pipefail
DIST_TAG="$(bun scripts/release-guard.ts)"
RELEASE_PLAN="$(bun scripts/release-guard.ts --json)"
DIST_TAG="$(jq -r '.distTag' <<< "$RELEASE_PLAN")"
echo "DIST_TAG=$DIST_TAG" >> "$GITHUB_ENV"
echo "Publishing with npm dist-tag $DIST_TAG"
jq '.packages' <<< "$RELEASE_PLAN"

- name: Typecheck
run: bun run typecheck
Expand Down Expand Up @@ -103,19 +109,40 @@ jobs:
fi
echo "npm $NPM_VERSION"

# npm publish does not verify tarball completeness; a files-array or
# build-layout regression must fail here, not ship as latest.
- name: Verify packed tarball contents
# Stage two package roots from the one verified build. The canonical
# package receives README.md; the legacy package receives the migration
# README under that same filename on npm.
- name: Prepare canonical and legacy npm packages
run: |
set -euo pipefail
FILES="$(npm pack --dry-run --json --ignore-scripts | jq -r '.[0].files[].path')"
for required in bin/langfuse.mjs dist/cli.js dist/contracts/catalog.json README.md; do
if ! printf '%s\n' "$FILES" | grep -qx "$required"; then
echo "::error::$required is missing from the npm tarball"
exit 1
fi
PACKAGE_ROOT="$GITHUB_WORKSPACE/.npm-packages"
bun scripts/npm-packages.ts "$PACKAGE_ROOT" "$RELEASE_SHA"
echo "PACKAGE_ROOT=$PACKAGE_ROOT" >> "$GITHUB_ENV"

# npm publish does not verify tarball completeness; a files-array,
# README selection, or build-layout regression must fail here.
- name: Verify both packed tarballs
run: |
set -euo pipefail
for package in canonical legacy; do
FILES="$(npm pack --dry-run --json --ignore-scripts "$PACKAGE_ROOT/$package" | jq -r '.[0].files[].path')"
for required in LICENSE bin/langfuse.mjs dist/cli.js dist/contracts/catalog.json README.md; do
if ! grep -qx "$required" <<< "$FILES"; then
echo "::error::$required is missing from the $package npm tarball"
exit 1
fi
done
done
echo "Tarball contents verified."
test "$(jq -r '.name' "$PACKAGE_ROOT/canonical/package.json")" = "@langfuse/cli"
test "$(jq -r '.name' "$PACKAGE_ROOT/legacy/package.json")" = "langfuse-cli"
grep -Fq '# `langfuse-cli` is deprecated' "$PACKAGE_ROOT/legacy/README.md"
if grep -Fq '# `langfuse-cli` is deprecated' "$PACKAGE_ROOT/canonical/README.md"; then
echo "::error::canonical npm README is marked deprecated"
exit 1
fi
diff -qr "$PACKAGE_ROOT/canonical/bin" "$PACKAGE_ROOT/legacy/bin"
diff -qr "$PACKAGE_ROOT/canonical/dist" "$PACKAGE_ROOT/legacy/dist"
echo "Both tarballs verified."

- name: Smoke-test the built CLI under Node and Bun
run: |
Expand All @@ -134,13 +161,59 @@ jobs:
IS_PRERELEASE: ${{ github.event.release.prerelease }}
run: |
set -euo pipefail
FINAL_TAG="$(bun scripts/release-guard.ts)"
FINAL_PLAN="$(bun scripts/release-guard.ts --json)"
FINAL_TAG="$(jq -r '.distTag' <<< "$FINAL_PLAN")"
if [ "$FINAL_TAG" != "$DIST_TAG" ]; then
echo "::error::dist-tag changed between verification and publish ($DIST_TAG -> $FINAL_TAG)"
exit 1
fi
echo "FINAL_PLAN=$FINAL_PLAN" >> "$GITHUB_ENV"
jq '.packages' <<< "$FINAL_PLAN"

# conformance:all already built dist/; --ignore-scripts avoids a second
# prepublishOnly build producing a different publish than was verified.
- name: Publish to npm
run: npm publish --ignore-scripts --tag "$DIST_TAG"
# Canonical publishes first so a partial failure never blocks old-name
# users from their previously working version.
- name: Publish @langfuse/cli to npm
run: |
set -euo pipefail
STATUS="$(jq -r '.packages.canonical.status' <<< "$FINAL_PLAN")"
VERSION="$(jq -r '.version' <<< "$FINAL_PLAN")"
if [ "$STATUS" = "already-published" ]; then
echo "@langfuse/cli@$VERSION was already published from this release; skipping."
else
npm publish "$PACKAGE_ROOT/canonical" --ignore-scripts --tag "$DIST_TAG"
fi

- name: Publish langfuse-cli compatibility package to npm
run: |
set -euo pipefail
STATUS="$(jq -r '.packages.legacy.status' <<< "$FINAL_PLAN")"
VERSION="$(jq -r '.version' <<< "$FINAL_PLAN")"
if [ "$STATUS" = "already-published" ]; then
echo "langfuse-cli@$VERSION was already published from this release; skipping."
else
npm publish "$PACKAGE_ROOT/legacy" --ignore-scripts --tag "$DIST_TAG"
fi

- name: Verify both publishes reached npm
env:
TAG_NAME: ${{ github.event.release.tag_name }}
IS_PRERELEASE: ${{ github.event.release.prerelease }}
run: |
set -euo pipefail
EXPECTED_VERSION="$(jq -r '.version' <<< "$FINAL_PLAN")"
for attempt in 1 2 3 4 5; do
if PUBLISHED_PLAN="$(bun scripts/release-guard.ts --json)" &&
[ "$(jq -r '.packages.canonical.status' <<< "$PUBLISHED_PLAN")" = "already-published" ] &&
[ "$(jq -r '.packages.legacy.status' <<< "$PUBLISHED_PLAN")" = "already-published" ] &&
[ "$(npm view "@langfuse/cli@$DIST_TAG" version)" = "$EXPECTED_VERSION" ] &&
[ "$(npm view "langfuse-cli@$DIST_TAG" version)" = "$EXPECTED_VERSION" ]; then
jq '.packages' <<< "$PUBLISHED_PLAN"
echo "Both npm packages verified."
exit 0
fi
sleep "$((attempt * 2))"
done
echo "::error::could not verify both npm packages from release commit $RELEASE_SHA"
exit 1
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ bun.lockb
.env
.env.local
.DS_Store
.npm-packages/
57 changes: 46 additions & 11 deletions MAINTENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,29 @@ bun run release
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 not on npm and the
tag is free; runs typecheck, both test suites, and the full conformance
build; then pushes a `chore(release): vX.Y.Z` commit plus the `vX.Y.Z` tag
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 a `chore(release): vX.Y.Z` commit plus the `vX.Y.Z` tag
and opens a **draft GitHub release** with generated notes.
2. **Publish the GitHub release**: edit the notes on GitHub and click
Publish. This is the release decision — nothing reaches npm before it.
3. **npm publish** (automatic): publishing the release triggers
[`release.yml`](.github/workflows/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, and `latest` never moving to
an older version), verifies the packed tarball contents, re-runs all
gates, and publishes via **npm trusted publishing (OIDC)** with provenance
attestations. No npm token exists anywhere.
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/cli` followed by the
`langfuse-cli` compatibility 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
Expand All @@ -119,15 +128,24 @@ Only when Actions is unavailable, publish directly from a machine:
bun run release -- --publish-local
```

This runs the same gates plus `npm pack --dry-run` and an explicit publish
confirmation, and requires interactive npm authentication (with OTP if the
package disallows tokens). It does not commit or tag; do that manually after.
This 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.

### One-time npm/GitHub configuration (required)

On npmjs.com → `langfuse-cli` → Settings:
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:

1. **Trusted Publisher** → GitHub: owner `langfuse`, repository `langfuse-cli`,
workflow filename `release.yml`, environment `npm-publish`.
Expand All @@ -148,3 +166,20 @@ closes this:
allow only tags matching `v*`.
4. Additionally, add a repository **ruleset restricting who can create `v*`
tags** (Settings → Rules → Rulesets) to maintainers.

### Legacy npm deprecation notice

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:

```sh
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.
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
<img width="2400" height="600" alt="hero-b" src="https://github.com/user-attachments/assets/85dcdba4-c037-4e3e-9f20-e39cde0a15ec" />

# langfuse-cli
# Langfuse CLI

Interact with the [Langfuse](https://langfuse.com) API from the command line.

## Install

```sh
# Run directly
npx langfuse-cli api <resource> <action>
npx @langfuse/cli api <resource> <action>
# via bun:
bunx --bun langfuse-cli api <resource> <action>
bunx --bun @langfuse/cli api <resource> <action>

# Or install globally
npm i -g langfuse-cli
npm i -g @langfuse/cli
# via bun:
bun add --global langfuse-cli
bun add --global @langfuse/cli

# then run
langfuse api <resource> <action>
langfuse --env .env api <resource> <action>
```

Note: the package was previously published under `langfuse-cli`. That package
is identical to thise one and will be updated alongside until we release a new major version.
We recommend using `@langfuse/cli` from now on.

## Authentication

The CLI needs the following parameters to work:
Expand Down
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions npm/legacy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Use `@langfuse/cli` instead of the package `langfuse-cli` from now on

The npm package has moved to
[`@langfuse/cli`](https://www.npmjs.com/package/@langfuse/cli). The executable
remains `langfuse`.

Run the current package directly:

```sh
npx @langfuse/cli api <resource> <action>
```

For a global installation, remove the legacy package first because both
packages provide the same `langfuse` executable:

```sh
npm uninstall -g langfuse-cli
npm install -g @langfuse/cli
```

See the [Langfuse CLI documentation](https://github.com/langfuse/langfuse-cli#readme)
for authentication, usage, and other installation options.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "langfuse-cli",
"name": "@langfuse/cli",
"version": "1.2.0",
"description": "Interact with Langfuse API from the command line",
"author": "Langfuse",
Expand All @@ -8,6 +8,9 @@
"type": "git",
"url": "git+https://github.com/langfuse/langfuse-cli.git"
},
"publishConfig": {
"access": "public"
},
"keywords": [
"langfuse",
"cli",
Expand Down
Loading