From 66ac0aa0844938fa78b8493125f4b19c6e1659e2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 20 Sep 2026 16:43:44 +0000 Subject: [PATCH 1/3] chore(release): @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 @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 @relayburn/sdk@4.1.0 @relayburn/mcp@4.1.0 relayburn@4.1.0 --- CHANGELOG.md | 2 + Cargo.lock | 6 +- Cargo.toml | 2 +- crates/relayburn-cli/Cargo.toml | 2 +- crates/relayburn-sdk-node/Cargo.toml | 2 +- packages/mcp/CHANGELOG.md | 2 + packages/mcp/package.json | 2 +- packages/relayburn/CHANGELOG.md | 2 + .../relayburn/npm/darwin-arm64/package.json | 2 +- .../relayburn/npm/darwin-x64/package.json | 2 +- .../npm/linux-arm64-gnu/package.json | 2 +- .../relayburn/npm/linux-x64-gnu/package.json | 2 +- packages/relayburn/package.json | 10 +-- packages/sdk-node/CHANGELOG.md | 2 + .../sdk-node/npm/darwin-arm64/package.json | 2 +- packages/sdk-node/npm/darwin-x64/package.json | 2 +- .../sdk-node/npm/linux-arm64-gnu/package.json | 2 +- .../sdk-node/npm/linux-x64-gnu/package.json | 2 +- packages/sdk-node/package.json | 10 +-- pnpm-lock.yaml | 80 +++++++++---------- 20 files changed, 73 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfc88a18..fde707b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Cargo.lock b/Cargo.lock index 9c269646..aa3d48dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1232,7 +1232,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "relayburn-cli" -version = "4.0.0" +version = "4.1.0" dependencies = [ "anyhow", "assert_cmd", @@ -1261,7 +1261,7 @@ dependencies = [ [[package]] name = "relayburn-sdk" -version = "4.0.0" +version = "4.1.0" dependencies = [ "anyhow", "hex", @@ -1284,7 +1284,7 @@ dependencies = [ [[package]] name = "relayburn-sdk-node" -version = "4.0.0" +version = "4.1.0" dependencies = [ "anyhow", "napi", diff --git a/Cargo.toml b/Cargo.toml index 7de39100..d61567bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/relayburn-cli/Cargo.toml b/crates/relayburn-cli/Cargo.toml index 3e3c3f4f..3b66fd4a 100644 --- a/crates/relayburn-cli/Cargo.toml +++ b/crates/relayburn-cli/Cargo.toml @@ -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 diff --git a/crates/relayburn-sdk-node/Cargo.toml b/crates/relayburn-sdk-node/Cargo.toml index c73ad61e..defb2495 100644 --- a/crates/relayburn-sdk-node/Cargo.toml +++ b/crates/relayburn-sdk-node/Cargo.toml @@ -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 } diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md index c2ee4731..aba049ed 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -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. diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 75999e34..b15e3a96 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -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", diff --git a/packages/relayburn/CHANGELOG.md b/packages/relayburn/CHANGELOG.md index 03297dd6..ff29e565 100644 --- a/packages/relayburn/CHANGELOG.md +++ b/packages/relayburn/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to `relayburn`. ## [Unreleased] +## [4.1.0] - 2026-09-20 + - `burn measure --harness --input --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. diff --git a/packages/relayburn/npm/darwin-arm64/package.json b/packages/relayburn/npm/darwin-arm64/package.json index 3348fa65..053f5c1a 100644 --- a/packages/relayburn/npm/darwin-arm64/package.json +++ b/packages/relayburn/npm/darwin-arm64/package.json @@ -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": { diff --git a/packages/relayburn/npm/darwin-x64/package.json b/packages/relayburn/npm/darwin-x64/package.json index a20b64ae..ca881dd9 100644 --- a/packages/relayburn/npm/darwin-x64/package.json +++ b/packages/relayburn/npm/darwin-x64/package.json @@ -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": { diff --git a/packages/relayburn/npm/linux-arm64-gnu/package.json b/packages/relayburn/npm/linux-arm64-gnu/package.json index cbcc8ef7..b8451685 100644 --- a/packages/relayburn/npm/linux-arm64-gnu/package.json +++ b/packages/relayburn/npm/linux-arm64-gnu/package.json @@ -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": { diff --git a/packages/relayburn/npm/linux-x64-gnu/package.json b/packages/relayburn/npm/linux-x64-gnu/package.json index 8d97daf5..68091a33 100644 --- a/packages/relayburn/npm/linux-x64-gnu/package.json +++ b/packages/relayburn/npm/linux-x64-gnu/package.json @@ -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": { diff --git a/packages/relayburn/package.json b/packages/relayburn/package.json index 16090610..48a625be 100644 --- a/packages/relayburn/package.json +++ b/packages/relayburn/package.json @@ -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-` packages.", "license": "Apache-2.0", "type": "module", @@ -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", diff --git a/packages/sdk-node/CHANGELOG.md b/packages/sdk-node/CHANGELOG.md index e32b39e8..bcb1d70f 100644 --- a/packages/sdk-node/CHANGELOG.md +++ b/packages/sdk-node/CHANGELOG.md @@ -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. diff --git a/packages/sdk-node/npm/darwin-arm64/package.json b/packages/sdk-node/npm/darwin-arm64/package.json index c0762dba..ef455cbe 100644 --- a/packages/sdk-node/npm/darwin-arm64/package.json +++ b/packages/sdk-node/npm/darwin-arm64/package.json @@ -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", diff --git a/packages/sdk-node/npm/darwin-x64/package.json b/packages/sdk-node/npm/darwin-x64/package.json index 6af5b48e..6075be67 100644 --- a/packages/sdk-node/npm/darwin-x64/package.json +++ b/packages/sdk-node/npm/darwin-x64/package.json @@ -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", diff --git a/packages/sdk-node/npm/linux-arm64-gnu/package.json b/packages/sdk-node/npm/linux-arm64-gnu/package.json index 79d2f25c..8db3e361 100644 --- a/packages/sdk-node/npm/linux-arm64-gnu/package.json +++ b/packages/sdk-node/npm/linux-arm64-gnu/package.json @@ -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", diff --git a/packages/sdk-node/npm/linux-x64-gnu/package.json b/packages/sdk-node/npm/linux-x64-gnu/package.json index 4625a3c7..87df7dcf 100644 --- a/packages/sdk-node/npm/linux-x64-gnu/package.json +++ b/packages/sdk-node/npm/linux-x64-gnu/package.json @@ -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", diff --git a/packages/sdk-node/package.json b/packages/sdk-node/package.json index 01fc4191..51e58ff4 100644 --- a/packages/sdk-node/package.json +++ b/packages/sdk-node/package.json @@ -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", @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c12fb21b..75ce8c13 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,17 +24,17 @@ importers: packages/relayburn: optionalDependencies: '@relayburn/cli-darwin-arm64': - specifier: 4.0.0 - version: 4.0.0 + specifier: 4.1.0 + version: 4.1.0 '@relayburn/cli-darwin-x64': - specifier: 4.0.0 - version: 4.0.0 + specifier: 4.1.0 + version: 4.1.0 '@relayburn/cli-linux-arm64-gnu': - specifier: 4.0.0 - version: 4.0.0 + specifier: 4.1.0 + version: 4.1.0 '@relayburn/cli-linux-x64-gnu': - specifier: 4.0.0 - version: 4.0.0 + specifier: 4.1.0 + version: 4.1.0 packages/sdk-node: devDependencies: @@ -46,17 +46,17 @@ importers: version: 0.25.12 optionalDependencies: '@relayburn/sdk-darwin-arm64': - specifier: 4.0.0 - version: 4.0.0 + specifier: 4.1.0 + version: 4.1.0 '@relayburn/sdk-darwin-x64': - specifier: 4.0.0 - version: 4.0.0 + specifier: 4.1.0 + version: 4.1.0 '@relayburn/sdk-linux-arm64-gnu': - specifier: 4.0.0 - version: 4.0.0 + specifier: 4.1.0 + version: 4.1.0 '@relayburn/sdk-linux-x64-gnu': - specifier: 4.0.0 - version: 4.0.0 + specifier: 4.1.0 + version: 4.1.0 packages: @@ -221,54 +221,54 @@ packages: engines: {node: '>= 10'} hasBin: true - '@relayburn/cli-darwin-arm64@4.0.0': - resolution: {integrity: sha512-mGkEGrDCjRRdgRrpD06JaEu/OXKKDUF4vMQZf1szRCzYB9KOBYxoiE+qmtOACYixDs8ZPtJyjjOJi5BEyyQrFQ==} + '@relayburn/cli-darwin-arm64@4.1.0': + resolution: {integrity: sha512-XqybQtpNmNc770cwq32ruSSB+QuTaeUjSCZYFv96IqXrHMwI79yFQwSFrCdfjkHxy3cY6JQ/78XI2o6/Q8htbA==} engines: {node: '>=22'} cpu: [arm64] os: [darwin] hasBin: true - '@relayburn/cli-darwin-x64@4.0.0': - resolution: {integrity: sha512-7c96/s937BlSExO9CU4tleI8tPlMlXz9Tl3YXLK2yQQCg+i+qtOtkEFVp54U6WaoNNTKKj3cXEDYjdkPdGPjag==} + '@relayburn/cli-darwin-x64@4.1.0': + resolution: {integrity: sha512-xKXTON4vp8Y9A+TH6cX1gddrWTZSR1SA2XN6ETZ4z8eIoMlNqeO4RzB6HA0x2O8Mtrfl93M1K+BPqPSzxMwLDw==} engines: {node: '>=22'} cpu: [x64] os: [darwin] hasBin: true - '@relayburn/cli-linux-arm64-gnu@4.0.0': - resolution: {integrity: sha512-foa2fIcLTNVcx2H3Gi3qWpJc2wVNcaA9//WSfJu8hoYoMpaj6xXc6fAE5jG5vfSeu7OxrUauL2ahMst7QsDelA==} + '@relayburn/cli-linux-arm64-gnu@4.1.0': + resolution: {integrity: sha512-NKLw6ULi4Avi65avl9ulvU6JPrJTpp2u3Jif+G2jOsNmYQnIg5YqXAgbLiM8QjkVcIB8rooC66gObgW+ejI86A==} engines: {node: '>=22'} cpu: [arm64] os: [linux] hasBin: true - '@relayburn/cli-linux-x64-gnu@4.0.0': - resolution: {integrity: sha512-MfFo4H2nOhVwFrsfoRJLwRmtd2WxiBQK21KgRmuyPqh5cFk2B8pt5EDWrlV3wM6h+djzE5cYP29wQfzj5CxElg==} + '@relayburn/cli-linux-x64-gnu@4.1.0': + resolution: {integrity: sha512-F36MZZrF6nT0GdnnSPI7gHIegNeZrGoJJv/2jpSXFUeCQ1WTVXwJQjxQVkztds2cPjrf19tgq7+4ZY/OIKhrIA==} engines: {node: '>=22'} cpu: [x64] os: [linux] hasBin: true - '@relayburn/sdk-darwin-arm64@4.0.0': - resolution: {integrity: sha512-l2E0SDcAkE+zd1apraRrVjIomBQS9x3Bl49CY7VfR6ZJF0EWQwQpD7zoAke2FkdpfHWEL/oJuleoGoojem+GOQ==} + '@relayburn/sdk-darwin-arm64@4.1.0': + resolution: {integrity: sha512-+lN6buiNTKXwWg82KBrpZDVuEll6SW7GbWfyjpCpe8/YlQMW1Al0d8vmKSeN0pdSZrJVm2Ibp/1mY1+DKIvqXg==} engines: {node: '>=22'} cpu: [arm64] os: [darwin] - '@relayburn/sdk-darwin-x64@4.0.0': - resolution: {integrity: sha512-IjfsojAG+Gv84J8a8gNpHabmZpVkXY/sy5r7fimvaaKL3iSW5IxXYfHvCwWUIOAbybebn7BqwIqKwhecpjBENw==} + '@relayburn/sdk-darwin-x64@4.1.0': + resolution: {integrity: sha512-Ibhf8uehIdk3SZoMi1CqepGZ74eRXHTEAOWQtUjyw3tqRVg/XDhBu/nJ1PNTKzP5zwuwY9G/x3QH/sT/PCEClg==} engines: {node: '>=22'} cpu: [x64] os: [darwin] - '@relayburn/sdk-linux-arm64-gnu@4.0.0': - resolution: {integrity: sha512-fFGHoM5LVO/P5MOgwYzJQpEt+FyGHkh9JK2kS8upv+xaLlq4T1zRbtqhO/nZcFq4RFh7H/QSyBthJ85VCvaBnw==} + '@relayburn/sdk-linux-arm64-gnu@4.1.0': + resolution: {integrity: sha512-wjXZRIE3BfHu2w8l7n7LO0p7vZwRf/ok9QH1Ozn+pGKlfLNjUfh2YdoXyK2LVuKkwOXUP8XEl5NPodA1YKYNHg==} engines: {node: '>=22'} cpu: [arm64] os: [linux] - '@relayburn/sdk-linux-x64-gnu@4.0.0': - resolution: {integrity: sha512-2NIqNNuHjIGzu/mDTv+1wOLbouipFhsjAnqMFIJL6yOi4hE1YH7sEdGBazDKPlrR3RjliBnbjUN4EzVNvIxohA==} + '@relayburn/sdk-linux-x64-gnu@4.1.0': + resolution: {integrity: sha512-mu/rLI5ZQbZCM8tdiMC/yI9S+whEUQ9ySW010/2CI3sGygXNovwekpNPBTfKGX3lX01/SmQHoEBrwWXaJyP/Zw==} engines: {node: '>=22'} cpu: [x64] os: [linux] @@ -371,28 +371,28 @@ snapshots: '@napi-rs/cli@2.18.4': {} - '@relayburn/cli-darwin-arm64@4.0.0': + '@relayburn/cli-darwin-arm64@4.1.0': optional: true - '@relayburn/cli-darwin-x64@4.0.0': + '@relayburn/cli-darwin-x64@4.1.0': optional: true - '@relayburn/cli-linux-arm64-gnu@4.0.0': + '@relayburn/cli-linux-arm64-gnu@4.1.0': optional: true - '@relayburn/cli-linux-x64-gnu@4.0.0': + '@relayburn/cli-linux-x64-gnu@4.1.0': optional: true - '@relayburn/sdk-darwin-arm64@4.0.0': + '@relayburn/sdk-darwin-arm64@4.1.0': optional: true - '@relayburn/sdk-darwin-x64@4.0.0': + '@relayburn/sdk-darwin-x64@4.1.0': optional: true - '@relayburn/sdk-linux-arm64-gnu@4.0.0': + '@relayburn/sdk-linux-arm64-gnu@4.1.0': optional: true - '@relayburn/sdk-linux-x64-gnu@4.0.0': + '@relayburn/sdk-linux-x64-gnu@4.1.0': optional: true '@types/node@22.19.18': From 476e2d1d2a55e8a9c2198964a93673330818c566 Mon Sep 17 00:00:00 2001 From: kjgbot Date: Sun, 20 Sep 2026 09:48:31 -0700 Subject: [PATCH 2/3] fix(release): respect protected main branch --- .github/workflows/publish.yml | 71 ++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a5c62f68..ca38d8ce 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -55,6 +55,7 @@ permissions: actions: read contents: write id-token: write + pull-requests: write concurrency: group: publish-${{ github.ref }} @@ -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 @@ -1402,8 +1403,10 @@ 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) are pushed explicitly. Do not push HEAD directly + # to `main`: the branch is protected and requires changes to arrive via + # a pull request. The generated version/changelog/lockfile commit is + # instead pushed to `release/v` and proposed in the next step. # # Tag scheme: # - `-v` for each of the 11 npm targets (mcp-v…, @@ -1413,13 +1416,16 @@ jobs: # - `relayburn-{sdk,cli}-v` 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 + release_tags=() while IFS=: read -r key dir; do [ -z "$key" ] && continue DIRS[$key]="$dir" @@ -1430,16 +1436,60 @@ jobs: 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" + git tag -a "$tag" -m "$NPM_NAME@$version" + release_tags+=("refs/tags/$tag") # 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" + release_tags+=("refs/tags/relayburn-sdk-v$version") + release_tags+=("refs/tags/relayburn-cli-v$version") fi done - git push origin HEAD --follow-tags + + # Push only the release tags here. Including HEAD in this command + # attempts to update protected `main` and makes a successful package + # publish appear failed after all registry writes have completed. + git push origin "${release_tags[@]}" + + release_branch="release/v$RELEASE_VERSION" + if git ls-remote --exit-code --heads origin "$release_branch" >/dev/null 2>&1; then + echo "Release branch $release_branch already exists; preserving it." + else + git push origin "HEAD:refs/heads/$release_branch" + 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: @@ -1471,6 +1521,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." From dd48b098c4899d424d0323601e681b4b1a79236b Mon Sep 17 00:00:00 2001 From: kjgbot Date: Sun, 20 Sep 2026 09:53:32 -0700 Subject: [PATCH 3/3] fix(release): make ref finalization idempotent --- .github/workflows/publish.yml | 88 ++++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 18 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ca38d8ce..fba3b718 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1403,10 +1403,11 @@ jobs: sleep 10 done - # Annotated tags (-a) are pushed explicitly. Do not push HEAD directly - # to `main`: the branch is protected and requires changes to arrive via - # a pull request. The generated version/changelog/lockfile commit is - # instead pushed to `release/v` and proposed in the next step. + # 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: # - `-v` for each of the 11 npm targets (mcp-v…, @@ -1425,41 +1426,92 @@ jobs: run: | set -euo pipefail declare -A DIRS - release_tags=() + 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 + 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") tag="$key-v$version" - git tag -a "$tag" -m "$NPM_NAME@$version" - release_tags+=("refs/tags/$tag") + 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" - release_tags+=("refs/tags/relayburn-sdk-v$version") - release_tags+=("refs/tags/relayburn-cli-v$version") + add_release_tag "relayburn-sdk-v$version" "relayburn-sdk@$version" + add_release_tag "relayburn-cli-v$version" "relayburn-cli@$version" fi done - # Push only the release tags here. Including HEAD in this command - # attempts to update protected `main` and makes a successful package - # publish appear failed after all registry writes have completed. - git push origin "${release_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" - if git ls-remote --exit-code --heads origin "$release_branch" >/dev/null 2>&1; then - echo "Release branch $release_branch already exists; preserving it." + 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 - git push origin "HEAD:refs/heads/$release_branch" + echo "All release refs already exist and match this release tree." fi echo "branch=$release_branch" >> "$GITHUB_OUTPUT"