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
127 changes: 116 additions & 11 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ permissions:
actions: read
contents: write
id-token: write
pull-requests: write

concurrency:
group: publish-${{ github.ref }}
Expand Down Expand Up @@ -1319,10 +1320,10 @@ jobs:
# `pnpm install --frozen-lockfile` on `main` fails.
#
# The lockfile change is amended into the version commit created by
# the "Commit version bumps" step earlier so the release is one
# atomic commit on `main`. The amend is safe because nothing has
# been pushed yet (the "Tag + push" step below pushes both the
# amended commit and the tags created against it).
# the "Commit version bumps" step earlier so the release metadata is
# one atomic commit. The amend is safe because nothing has been pushed
# yet. The finalization step publishes immutable tags, then puts this
# commit on a release branch and opens a PR because `main` is protected.
#
# `--lockfile-only` skips node_modules churn — we already installed
# dependencies near the top of the job. On dry runs there's nothing
Expand Down Expand Up @@ -1402,8 +1403,11 @@ jobs:
sleep 10
done

# Annotated tags (-a) so `git push --follow-tags` actually pushes them;
# lightweight tags are skipped by --follow-tags.
# Annotated tags (-a) and the release branch are pushed as one atomic
# transaction. Do not push HEAD directly to `main`: the branch is
# protected and requires changes to arrive via a pull request. On a
# recovery run, existing refs are reused only when their release tree
# matches the tree generated by this run.
#
# Tag scheme:
# - `<key>-v<ver>` for each of the 11 npm targets (mcp-v…,
Expand All @@ -1413,33 +1417,131 @@ jobs:
# - `relayburn-{sdk,cli}-v<ver>` for the two crates.io crates,
# reusing the same lockstep version. Disambiguated from the npm
# `sdk-v…` / (legacy) `cli-v…` tags via the `relayburn-` prefix.
- name: Tag + push
- name: Finalize release refs
id: finalize
if: ${{ github.event.inputs.dry_run != 'true' && (github.event.inputs.version != 'none' || github.event.inputs.custom_version != '') }}
env:
TARGETS: ${{ steps.targets.outputs.targets }}
RELEASE_VERSION: ${{ steps.bump.outputs.release_version }}
run: |
set -euo pipefail
declare -A DIRS
missing_tags=()
canonical_commit=""
expected_tree=$(git rev-parse 'HEAD^{tree}')
while IFS=: read -r key dir; do
[ -z "$key" ] && continue
DIRS[$key]="$dir"
done <<< "$TARGETS"

add_release_tag() {
local tag="$1"
local message="$2"
local tagged_commit tagged_tree

if git show-ref --verify --quiet "refs/tags/$tag"; then
tagged_commit=$(git rev-parse "refs/tags/$tag^{commit}")
tagged_tree=$(git rev-parse "refs/tags/$tag^{tree}")
if [ "$tagged_tree" != "$expected_tree" ]; then
echo "::error title=Release tag conflict::$tag already exists but its release tree does not match this run. Refusing to move an immutable tag."
exit 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recovery tree check blocks tag reuse

Medium Severity

Finalize release refs reuses existing tags and release/v* only when the entire HEAD tree matches. Recovery already allows publish.yml to change so a workflow repair can resume, and that delta makes the trees differ, so already-pushed immutable tags are treated as conflicts. The job then fails after registry publish and never opens the metadata PR from the tagged release commit.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit dd48b09. Configure here.

fi
if [ -n "$canonical_commit" ] && [ "$canonical_commit" != "$tagged_commit" ]; then
echo "::error title=Inconsistent release tags::$tag points to $tagged_commit, while another release tag points to $canonical_commit."
exit 1
fi
canonical_commit="$tagged_commit"
echo "Reusing verified release tag $tag at $tagged_commit."
else
missing_tags+=("$tag:$message")
fi
}

for entry in ${{ steps.bump.outputs.versions }}; do
key="${entry%%:*}"
version="${entry##*:}"
dir="${DIRS[$key]}"
NPM_NAME=$(node -p "require('./$dir/package.json').name")
git tag -a "$key-v$version" -m "$NPM_NAME@$version"
tag="$key-v$version"
add_release_tag "$tag" "$NPM_NAME@$version"
# crates.io tags. Lockstep, so reuse $version. Only emit on the
# `sdk` (napi umbrella) entry to avoid duplicate tags across
# the 11 npm targets.
if [ "$key" = "sdk" ]; then
git tag -a "relayburn-sdk-v$version" -m "relayburn-sdk@$version"
git tag -a "relayburn-cli-v$version" -m "relayburn-cli@$version"
add_release_tag "relayburn-sdk-v$version" "relayburn-sdk@$version"
add_release_tag "relayburn-cli-v$version" "relayburn-cli@$version"
fi
done
git push origin HEAD --follow-tags

# If this is a recovery after refs were published, anchor any missing
# refs to the already-verified immutable release commit. Otherwise the
# release commit produced by this run is canonical.
if [ -z "$canonical_commit" ]; then
canonical_commit=$(git rev-parse HEAD)
fi

for item in "${missing_tags[@]}"; do
tag="${item%%:*}"
message="${item#*:}"
git tag -a "$tag" "$canonical_commit" -m "$message"
done

release_branch="release/v$RELEASE_VERSION"
push_refspecs=()
remote_branch_commit=$(git ls-remote --heads origin "refs/heads/$release_branch" | awk '{print $1}')
if [ -n "$remote_branch_commit" ]; then
git fetch --no-tags origin "refs/heads/$release_branch"
branch_tree=$(git rev-parse 'FETCH_HEAD^{tree}')
if [ "$branch_tree" != "$expected_tree" ]; then
echo "::error title=Release branch conflict::$release_branch already exists but its release tree does not match this run."
exit 1
fi
echo "Reusing verified release branch $release_branch at $remote_branch_commit."
else
push_refspecs+=("$canonical_commit:refs/heads/$release_branch")
fi

for item in "${missing_tags[@]}"; do
tag="${item%%:*}"
push_refspecs+=("refs/tags/$tag:refs/tags/$tag")
done

if [ "${#push_refspecs[@]}" -gt 0 ]; then
# The branch and all missing tags either land together or not at
# all, preventing a retry from observing partially finalized refs.
git push --atomic origin "${push_refspecs[@]}"
else
echo "All release refs already exist and match this release tree."
fi
echo "branch=$release_branch" >> "$GITHUB_OUTPUT"

- name: Open release metadata pull request
id: release_pr
if: ${{ steps.finalize.outputs.branch != '' }}
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
RELEASE_BRANCH: ${{ steps.finalize.outputs.branch }}
RELEASE_VERSION: ${{ steps.bump.outputs.release_version }}
run: |
set -euo pipefail
pr_url=$(gh pr list \
--base main \
--head "$RELEASE_BRANCH" \
--state open \
--json url \
--jq '.[0].url // empty')

if [ -z "$pr_url" ]; then
pr_url=$(gh pr create \
--base main \
--head "$RELEASE_BRANCH" \
--title "chore(release): v$RELEASE_VERSION" \
--body "Generated by the publish workflow after the v$RELEASE_VERSION registry artifacts and release tags were verified. This PR records the version, changelog, and lockfile updates on protected main.")
fi

echo "url=$pr_url" >> "$GITHUB_OUTPUT"
echo "Release metadata PR: $pr_url"

- name: Summary
env:
Expand Down Expand Up @@ -1471,6 +1573,9 @@ jobs:
echo ""
echo "- **dist-tag**: \`${{ github.event.inputs.tag }}\`"
echo "- **dry run**: \`${{ github.event.inputs.dry_run }}\`"
if [ -n "${{ steps.release_pr.outputs.url }}" ]; then
echo "- **release metadata PR**: ${{ steps.release_pr.outputs.url }}"
fi
echo ""
if [ "${{ github.event.inputs.dry_run }}" != "true" ]; then
echo "Next step: verify the published artifact by running the \`Verify Publish\` workflow."
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Cross-package release notes for relayburn. Package changelogs contain package-le

## [Unreleased]

## [4.1.0] - 2026-09-20

- `burn measure` and `@relayburn/sdk.measureSession()` turn one explicit Claude Code, Codex, or OpenCode session source into a versioned per-model token/cost document without discovery or a ledger; incomplete and zero-turn inputs fail closed.
- `@relayburn/sdk` exposes `turnSpanTree`, `sessionSpanTrees`, `flowGraph`, and `contextDelta`, matching the Rust SDK verbs for span trees, inference-flow DAGs, and context-window deltas.
- `@relayburn/mcp` tests compile TypeScript before running, so the package suite executes on a clean checkout.
Expand Down
6 changes: 3 additions & 3 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/*"]

[workspace.package]
version = "4.0.0"
version = "4.1.0"
edition = "2021"
rust-version = "1.94"
license = "Apache-2.0"
Expand Down
2 changes: 1 addition & 1 deletion crates/relayburn-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ path = "src/lib.rs"
# the workspace MAJOR.MINOR on every release so the local workspace
# path (currently 1.10.0) and the published `relayburn-sdk` on
# crates.io always satisfy the dep at publish time.
relayburn-sdk = { path = "../relayburn-sdk", version = "4.0" }
relayburn-sdk = { path = "../relayburn-sdk", version = "4.1" }

# clap v4 derive — argument parsing root and subcommand dispatch. The
# scaffold defines globals + subcommand stubs only; per-command flag
Expand Down
2 changes: 1 addition & 1 deletion crates/relayburn-sdk-node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ crate-type = ["cdylib", "rlib"]
# this crate is consumed exclusively by the napi-rs CI matrix in #247-b
# to produce the per-platform `.node` artifacts under
# `packages/sdk-node/`.
relayburn-sdk = { path = "../relayburn-sdk", version = "4.0" }
relayburn-sdk = { path = "../relayburn-sdk", version = "4.1" }
napi = { version = "2", default-features = false, features = ["napi6", "tokio_rt", "serde-json"] }
napi-derive = "2"
serde = { workspace = true }
Expand Down
2 changes: 2 additions & 0 deletions packages/mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ All notable changes to `@relayburn/mcp`.

## [Unreleased]

## [4.1.0] - 2026-09-20

- Package tests compile TypeScript before running, so `pnpm run test` executes the MCP suite on a clean checkout.
- Read-tool responses now include `ledgerFreshness` with the ledger's last-write timestamp, threshold, and stale flag.
- Cost output recognizes Claude 5 and GPT-5.6 models, prefers first-party tariffs, and applies long-context price tiers.
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@relayburn/mcp",
"version": "4.0.0",
"version": "4.1.0",
"description": "MCP (Model Context Protocol) server exposing read-only relayburn ledger queries for in-session self-query",
"license": "Apache-2.0",
"type": "module",
Expand Down
2 changes: 2 additions & 0 deletions packages/relayburn/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ All notable changes to `relayburn`.

## [Unreleased]

## [4.1.0] - 2026-09-20

- `burn measure --harness <name> --input <path> --json` emits Cloud-ready per-model token and cost metrics for one explicit session without discovery or ledger state.
- Cost output recognizes Claude 5 and GPT-5.6 models, prefers first-party tariffs, and applies long-context price tiers.
- `burn hotspots --findings` identifies unknown pricing and ranks unpriced sessions by token volume instead of $0.00.
Expand Down
2 changes: 1 addition & 1 deletion packages/relayburn/npm/darwin-arm64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@relayburn/cli-darwin-arm64",
"version": "4.0.0",
"version": "4.1.0",
"description": "Prebuilt `burn` binary for darwin-arm64.",
"license": "Apache-2.0",
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion packages/relayburn/npm/darwin-x64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@relayburn/cli-darwin-x64",
"version": "4.0.0",
"version": "4.1.0",
"description": "Prebuilt `burn` binary for darwin-x64.",
"license": "Apache-2.0",
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion packages/relayburn/npm/linux-arm64-gnu/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@relayburn/cli-linux-arm64-gnu",
"version": "4.0.0",
"version": "4.1.0",
"description": "Prebuilt `burn` binary for linux-arm64-gnu (glibc).",
"license": "Apache-2.0",
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion packages/relayburn/npm/linux-x64-gnu/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@relayburn/cli-linux-x64-gnu",
"version": "4.0.0",
"version": "4.1.0",
"description": "Prebuilt `burn` binary for linux-x64-gnu (glibc).",
"license": "Apache-2.0",
"bin": {
Expand Down
10 changes: 5 additions & 5 deletions packages/relayburn/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "relayburn",
"version": "4.0.0",
"version": "4.1.0",
"description": "Token usage & cost attribution for agent CLIs (installs the `burn` command). Resolves a prebuilt Rust binary from the `@relayburn/cli-<platform>` packages.",
"license": "Apache-2.0",
"type": "module",
Expand All @@ -20,10 +20,10 @@
"node": ">=22"
},
"optionalDependencies": {
"@relayburn/cli-darwin-arm64": "4.0.0",
"@relayburn/cli-darwin-x64": "4.0.0",
"@relayburn/cli-linux-arm64-gnu": "4.0.0",
"@relayburn/cli-linux-x64-gnu": "4.0.0"
"@relayburn/cli-darwin-arm64": "4.1.0",
"@relayburn/cli-darwin-x64": "4.1.0",
"@relayburn/cli-linux-arm64-gnu": "4.1.0",
"@relayburn/cli-linux-x64-gnu": "4.1.0"
},
"repository": {
"type": "git",
Expand Down
2 changes: 2 additions & 0 deletions packages/sdk-node/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## [Unreleased]

## [4.1.0] - 2026-09-20

- `measureSession({ harness, inputPath })` returns `burn.session-metrics.v1` for one exact session without discovery or a ledger; OpenCode accepts its selected metadata file within the complete per-session storage tree and incomplete inputs fail closed.
- `turnSpanTree()`, `sessionSpanTrees()`, `flowGraph()`, and `contextDelta()` expose the matching Rust SDK verbs; token counters in their JSON output promote through BigInt.
- `ledgerFreshness()` exposes the ledger's last-write timestamp, configured threshold, and stale flag for Node and MCP presenters.
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-node/npm/darwin-arm64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@relayburn/sdk-darwin-arm64",
"version": "4.0.0",
"version": "4.1.0",
"description": "Prebuilt napi-rs binding for @relayburn/sdk on darwin-arm64.",
"license": "Apache-2.0",
"main": "relayburn-sdk.darwin-arm64.node",
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-node/npm/darwin-x64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@relayburn/sdk-darwin-x64",
"version": "4.0.0",
"version": "4.1.0",
"description": "Prebuilt napi-rs binding for @relayburn/sdk on darwin-x64.",
"license": "Apache-2.0",
"main": "relayburn-sdk.darwin-x64.node",
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-node/npm/linux-arm64-gnu/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@relayburn/sdk-linux-arm64-gnu",
"version": "4.0.0",
"version": "4.1.0",
"description": "Prebuilt napi-rs binding for @relayburn/sdk on linux-arm64-gnu (glibc).",
"license": "Apache-2.0",
"main": "relayburn-sdk.linux-arm64-gnu.node",
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-node/npm/linux-x64-gnu/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@relayburn/sdk-linux-x64-gnu",
"version": "4.0.0",
"version": "4.1.0",
"description": "Prebuilt napi-rs binding for @relayburn/sdk on linux-x64-gnu (glibc).",
"license": "Apache-2.0",
"main": "relayburn-sdk.linux-x64-gnu.node",
Expand Down
10 changes: 5 additions & 5 deletions packages/sdk-node/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@relayburn/sdk",
"version": "4.0.0",
"version": "4.1.0",
"description": "Embeddable Relayburn SDK — napi-rs bindings over the Rust relayburn-sdk crate (2.x). Drop-in replacement for the TS @relayburn/sdk@1.x.",
"license": "Apache-2.0",
"type": "module",
Expand Down Expand Up @@ -44,10 +44,10 @@
]
},
"optionalDependencies": {
"@relayburn/sdk-darwin-arm64": "4.0.0",
"@relayburn/sdk-darwin-x64": "4.0.0",
"@relayburn/sdk-linux-arm64-gnu": "4.0.0",
"@relayburn/sdk-linux-x64-gnu": "4.0.0"
"@relayburn/sdk-darwin-arm64": "4.1.0",
"@relayburn/sdk-darwin-x64": "4.1.0",
"@relayburn/sdk-linux-arm64-gnu": "4.1.0",
"@relayburn/sdk-linux-x64-gnu": "4.1.0"
},
"devDependencies": {
"@napi-rs/cli": "^2.18.4",
Expand Down
Loading
Loading