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
303 changes: 303 additions & 0 deletions .github/workflows/beta-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
# Setup on @langfuse/cli: trusted publisher beta-release.yml with environment
# npm-publish-beta. Allow source branches and require release reviewers on this
# environment: dispatch executes workflow code from the selected branch.
# Run via Actions > Beta release > Run workflow, then select the source branch.
# Merge this workflow into the default branch to expose the manual run button.
name: Beta release

on:
workflow_dispatch:
inputs:
dry_run:
description: Run all checks and show the version without publishing
type: boolean
default: true

permissions: {}

defaults:
run:
shell: bash

# Serialize beta version allocation and publishing across manual runs.
concurrency:
group: npm-beta-publish
queue: max
cancel-in-progress: false

jobs:
beta-version:
name: Select beta version
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
version: ${{ steps.plan.outputs.version }}
skip: ${{ steps.plan.outputs.skip }}
steps:
- name: Checkout selected commit
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.sha }}
persist-credentials: false
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14
no-cache: true
- name: Install dependencies
run: bun ci
- name: Allocate beta version or recognize a retry
id: plan
run: bun scripts/beta-release.ts >> "$GITHUB_OUTPUT"

preparation:
name: Prepare beta release
needs: beta-version
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
version: ${{ needs.beta-version.outputs.version }}
skip: ${{ needs.beta-version.outputs.skip }}
dist-tag: ${{ steps.plan.outputs.dist-tag }}
artifact-id: ${{ steps.upload.outputs.artifact-id }}
env:
TAG_NAME: ${{ format('v{0}', needs.beta-version.outputs.version) }}
IS_PRERELEASE: "true"
RELEASE_SHA: ${{ github.sha }}
steps:
- name: Checkout selected commit
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.sha }}
persist-credentials: false

- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14
# This workflow publishes an artifact; never restore caches into it.
no-cache: true

- name: Install dependencies
run: bun ci

- name: Set beta package version in the runner
env:
BETA_VERSION: ${{ needs.beta-version.outputs.version }}
run: bun scripts/beta-release.ts --apply

- name: Verify beta release policy and derive publish plan
id: plan
run: |
set -euo pipefail
RELEASE_PLAN="$(bun scripts/beta-release.ts --plan)"
DIST_TAG="$(jq -r '.distTag' <<< "$RELEASE_PLAN")"
echo "DIST_TAG=$DIST_TAG" >> "$GITHUB_ENV"
echo "FINAL_PLAN=$RELEASE_PLAN" >> "$GITHUB_ENV"
echo "dist-tag=$DIST_TAG" >> "$GITHUB_OUTPUT"
echo "Selected npm dist-tag $DIST_TAG"
jq '.packages' <<< "$RELEASE_PLAN"

- name: Typecheck
run: bun run typecheck

- name: Run tests
run: bun test

- name: Build and fake-call every endpoint in every API snapshot
run: bun run conformance:all

- name: Set up Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
# This workflow publishes an artifact; never restore caches into it.
package-manager-cache: false

- name: Verify npm supports trusted publishing (requires npm >= 11.5.1)
run: |
set -euo pipefail
NPM_VERSION="$(npm --version)"
MINIMUM="11.5.1"
if [ "$(printf '%s\n' "$MINIMUM" "$NPM_VERSION" | sort -V | head -n1)" != "$MINIMUM" ]; then
echo "::error::npm $NPM_VERSION is too old for trusted publishing (need >= $MINIMUM)"
exit 1
fi
echo "npm $NPM_VERSION"

# Stage both 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
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
test "$(jq -r '.name' "$PACKAGE_ROOT/canonical/package.json")" = "@langfuse/cli"
test "$(jq -r '.name' "$PACKAGE_ROOT/legacy/package.json")" = "langfuse-cli"
cmp README.md "$PACKAGE_ROOT/canonical/README.md"
cmp npm/legacy/README.md "$PACKAGE_ROOT/legacy/README.md"
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: |
set -euo pipefail
node bin/langfuse.mjs --version
bun bin/langfuse.mjs --help > /dev/null
echo "Smoke test passed."

- name: Pack canonical package for publishing
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/beta-release"
TARBALL="$(npm pack "$PACKAGE_ROOT/canonical" --ignore-scripts --json --pack-destination "$RUNNER_TEMP/beta-release" | jq -r '.[0].filename')"
mv "$RUNNER_TEMP/beta-release/$TARBALL" "$RUNNER_TEMP/beta-release/package.tgz"

- name: Upload prepared package
id: upload
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: beta-package-${{ github.run_attempt }}
path: ${{ runner.temp }}/beta-release/package.tgz
if-no-files-found: error
retention-days: 7

- name: Report release plan
run: |
set -euo pipefail
VERSION="$(jq -r '.version' <<< "$FINAL_PLAN")"
{
echo "### Release plan"
echo ""
echo "Version: $VERSION"
echo ""
echo "npm dist-tag: $DIST_TAG"
echo ""
jq -r '.packages[] | "- \(.name): \(.status)"' <<< "$FINAL_PLAN"
echo "- langfuse-cli: validated, publishing disabled"
} | tee -a "$GITHUB_STEP_SUMMARY"

publish:
name: Publish beta to npm
needs: preparation
if: ${{ !inputs.dry_run && needs.preparation.outputs.skip == 'false' }}
runs-on: ubuntu-latest
environment: npm-publish-beta
permissions:
contents: read
id-token: write
env:
TAG_NAME: ${{ format('v{0}', needs.preparation.outputs.version) }}
IS_PRERELEASE: "true"
RELEASE_SHA: ${{ github.sha }}
DIST_TAG: ${{ needs.preparation.outputs.dist-tag }}
steps:
- name: Checkout selected commit
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.sha }}
persist-credentials: false

- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14
# This workflow publishes an artifact; never restore caches into it.
no-cache: true

- name: Install dependencies
run: bun ci

- name: Set beta package version in the runner
env:
BETA_VERSION: ${{ needs.preparation.outputs.version }}
run: bun scripts/beta-release.ts --apply

- name: Set up Node (bundles the npm that performs the publish)
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
# This workflow publishes an artifact; never restore caches into it.
package-manager-cache: false

- name: Verify npm supports trusted publishing (requires npm >= 11.5.1)
run: |
set -euo pipefail
NPM_VERSION="$(npm --version)"
MINIMUM="11.5.1"
if [ "$(printf '%s\n' "$MINIMUM" "$NPM_VERSION" | sort -V | head -n1)" != "$MINIMUM" ]; then
echo "::error::npm $NPM_VERSION is too old for trusted publishing (need >= $MINIMUM)"
exit 1
fi
echo "npm $NPM_VERSION"

- name: Download prepared package
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
artifact-ids: ${{ needs.preparation.outputs.artifact-id }}
path: ${{ runner.temp }}/beta-release

# The early guard ran before ~5 minutes of gates; re-check against the
# registry state at publish time so changes made outside this workflow
# cannot slip a dist-tag backwards.
- name: Re-verify release policy immediately before publish
run: |
set -euo pipefail
FINAL_PLAN="$(bun scripts/beta-release.ts --plan)"
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"

# Publish the package built and checked by preparation without rebuilding it.
- 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 "$RUNNER_TEMP/beta-release/package.tgz" --ignore-scripts --tag "$DIST_TAG"
fi
Comment thread
tobi12345 marked this conversation as resolved.

- name: Verify beta publish reached npm
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/beta-release.ts --plan)" &&
[ "$(jq -r '.packages.canonical.status' <<< "$PUBLISHED_PLAN")" = "already-published" ] &&
[ "$(npm view "@langfuse/cli@$DIST_TAG" version)" = "$EXPECTED_VERSION" ]; then
jq '.packages' <<< "$PUBLISHED_PLAN"
echo "Beta npm package verified."
exit 0
fi
sleep "$((attempt * 2))"
done
echo "::error::could not verify beta package from release commit $RELEASE_SHA"
exit 1
Loading
Loading