Skip to content

ci: cut changelogs as part of the release commit - #299

Merged
willwashburn merged 1 commit into
mainfrom
claude/relaycast-changelog-cut-automation
Jul 25, 2026
Merged

ci: cut changelogs as part of the release commit#299
willwashburn merged 1 commit into
mainfrom
claude/relaycast-changelog-cut-automation

Conversation

@willwashburn

@willwashburn willwashburn commented Jul 25, 2026

Copy link
Copy Markdown
Member

Follow-up to #298, which filed 6.0.4–6.2.0 by hand. This stops it recurring.

Problem

publish-npm.yml bumps versions and commits, but nothing moves the curated [Unreleased] block — CHANGELOG.md isn't even in the git add. Four releases went to npm with no heading in any changelog while their entries piled up as "pending". Relay's publish.yml has solved this for a while; this ports the idea with the multi-package handling relaycast needs.

Changes

scripts/cut-changelog.mjs — moves the curated block under the released version, restores a bare [Unreleased], sweeps the root changelog plus every lockstep packages/*/CHANGELOG.md.

  • Curated entries are authoritative (AGENTS.md). Commit subjects are a fallback for the root changelog only, when its block is empty — a package with nothing pending gets no heading rather than duplicated noise.
  • packages/sdk-rust is excluded (independent crates.io version line). Package changelogs added later are swept by default.
  • Prereleases (6.3.0-beta.1) leave the block pending.
  • A pending level higher than the actual bump ([Unreleased - Minor] released as a patch) emits a warning. It never fails: the step runs after npm publish, so a hard failure would strand a published release without a tag.

publish-npm.yml — runs the script in create-release before the release commit, and stages the changelogs alongside the version bumps.

The rebase-onto-main retry loop needed one change. Version metadata never conflicted, but a changelog does — any PR curating [Unreleased] during the multi-minute build touches the same lines. On a conflict confined to CHANGELOG.md files it now takes main's copy, re-cuts it, and continues; anything else still aborts.

AGENTS.md — the "when a release is cut, move the pending entries" bullet now points at the script, so nobody hand-cuts a release.

Verification

Run against the real tree (--dry-run and for real, reverted after):

cutting v6.2.1 (2026-07-25), pending since v6.2.0
CHANGELOG.md: cut [6.2.1] (curated)
packages/engine/CHANGELOG.md: nothing pending, left unchanged
packages/sdk-swift/CHANGELOG.md: nothing pending, left unchanged
packages/sdk-typescript/CHANGELOG.md: nothing pending, left unchanged
packages/types/CHANGELOG.md: cut [6.2.1] (curated)

Also exercised in a throwaway clone: prerelease skip; the commit-subject fallback with an emptied block (produced ### Fixed / - Remove raw NUL byte from source and correct deliver fixture — PR ref stripped, docs: commit skipped); the level-mismatch warning; and a forced mid-release conflict where a concurrent PR raised the pending heading to - Minor — the resolution path kept that PR's entries, cut them into the release, and completed the rebase.

Not verifiable until a real release: the workflow wiring itself (job ordering, needs.build.outputs.new_version). The script and the conflict block were tested directly.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TRUCraRPGnM6CP59qoxyd6


Generated by Claude Code

Review in cubic

The publish workflow bumped versions and committed, but nothing moved the
curated [Unreleased] block, so 6.0.4 through 6.2.0 shipped to npm with no
heading in any changelog and their entries piled up as "pending".

Add scripts/cut-changelog.mjs and run it in create-release before the
release commit. It moves the curated block under the released version,
restores a bare [Unreleased], and sweeps the root changelog plus every
lockstep packages/*/CHANGELOG.md. Notes:

- Curated entries are authoritative; commit subjects are a fallback for
  the root changelog only when its block is empty, so a package with
  nothing pending gets no heading instead of duplicated noise.
- packages/sdk-rust is excluded — it ships on its own crates.io version
  line. New package changelogs are swept by default.
- Prereleases leave the block pending.
- A pending level higher than the actual bump warns rather than fails;
  the step runs after npm publish, so it must never break a release.
- The rebase-onto-main retry loop now resolves a changelog conflict (a PR
  curating [Unreleased] mid-release) by taking main's copy and re-cutting
  it, instead of failing a release that is already published.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRUCraRPGnM6CP59qoxyd6
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@willwashburn, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f5b1758b-02d4-481c-a0bb-5d6880a5c504

📥 Commits

Reviewing files that changed from the base of the PR and between 04ce251 and f4b73ac.

📒 Files selected for processing (3)
  • .github/workflows/publish-npm.yml
  • AGENTS.md
  • scripts/cut-changelog.mjs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/relaycast-changelog-cut-automation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@willwashburn
willwashburn merged commit e8bb5ec into main Jul 25, 2026
5 checks passed
@willwashburn
willwashburn deleted the claude/relaycast-changelog-cut-automation branch July 25, 2026 12:26

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f4b73acee9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/cut-changelog.mjs
}

const version = flag('version');
if (!version || !/^\d+\.\d+\.\d+(?:-[\w.]+)?$/.test(version)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Accept hyphens in valid prerelease versions

When workflow_dispatch supplies a custom version such as 6.3.0-rc-1, the build and npm publishing steps accept that valid SemVer prerelease, but this narrower regex rejects it in the subsequent create-release job. Because changelog cutting runs after publishing, the workflow then stops before creating the release commit, tag, and GitHub release, leaving already-published packages stranded; validate with npm's SemVer parser rather than this regex.

AGENTS.md reference: AGENTS.md:L42-L42

Useful? React with 👍 / 👎.

willwashburn pushed a commit that referenced this pull request Jul 25, 2026
Main refiled the pending 6.0.4–6.2.0 entries under release headings (#298)
and added scripts/cut-changelog.mjs (#299). The textual merge landed this
branch's types entry inside the now-released [6.2.0] block, which would have
credited an unreleased change to a shipped version — moved it back under
[Unreleased], and raised that heading from Patch to Minor since widening an
exported const/union is additive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CxUMkEa2foSTbw88sqEFWA
willwashburn pushed a commit that referenced this pull request Jul 28, 2026
…nd API docs

Addresses the three review findings on #300. All were valid.

**Agent sockets lost the new dimensions (P2).** `AgentClient.connect()` copied
only `agentRelayDistinctId` from its inherited `HttpClient`, so a
`/v1/node/ws` session reported `ws_session_started` as unauthenticated and
without user/machine/org — even though the same client's HTTP requests carried
them. Same class of bug as the `RelayCast` one this PR already fixed; I'd
missed the sibling.

Rather than adding the four fields to a third hand-copy, the root cause was
the duplication itself: three places (`withApiKey`, `RelayCast`, `AgentClient`)
each spread origin fields by hand, so any new dimension had to be remembered in
all three. Added `HttpClient.internalOrigin` as the single source and pointed
all three at it — a new dimension now reaches every socket at once.

The regression test asserts the actual agent socket URL, and I verified it goes
red against the old hand-copy before going green on the fix.

**Changelog claimed a shipped release (P1).** My rebase resolution put these
entries inside the dated `[6.2.0]` section — main's #299 had restructured the
file underneath me — falsely telling users the published package supports these
options, and leaving the release tooling nothing to carry forward. Moved to
`[Unreleased]`, raised Patch → Minor per the monotonic rule in AGENTS.md, and
added the package-level API detail to the engine and TypeScript SDK changelogs
that AGENTS.md requires.

**Undocumented API inputs (P1).** New accepted headers and WS query params were
in neither `README.md` nor `openapi.yaml`, so non-SDK callers had no documented
contract. Both now carry the full table, the charset/length rules, and why the
query-param forms exist.

Both docs were also stale independently: each described a `harness` option and
an `X-Relaycast-Harness` header that the engine no longer reads anywhere (it
was superseded by `X-Relaycast-Origin-Actor`). Corrected to the actual contract
while adding the identity fields.

Validation: turbo lint+build 17/17, engine 518 passed, SDK 414 passed,
openapi.yaml re-parsed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UEGpCHvkLVy7AhPgjrCbcG
willwashburn added a commit that referenced this pull request Jul 28, 2026
…ty (#300)

* feat(telemetry): accept caller-declared user and org identity

A relaycast workspace is an API-key row — there is no user table — so server
telemetry could only ever report workspaces, never people or companies.

Callers that do know who is driving a request (the Agent Relay CLI and broker,
after `agent-relay cloud login`) can now declare it:
`X-Agent-Relay-User-Id` / `-Org-Id` / `-Org-Slug`, or the matching
`agent_relay_*` query params on a WebSocket upgrade, since browsers can't set
custom headers on a handshake.

`emitServerEvent` records them as `actor_user_id` / `actor_org_id` /
`actor_org_slug` and prefers the user id as the distinct id, so hosted events
land on the same PostHog person as that user's CLI and broker events. These
are analytics dimensions only and never affect authorization — malformed
values are dropped rather than truncated.

The TS SDK accepts `agentRelayUserId` / `agentRelayOrgId` /
`agentRelayOrgSlug`, and a supplied user id doubles as the distinct id so a
host needs to set only one. Identity resolution is centralized in
`resolveAgentRelayIdentity` rather than threaded field-by-field.

Fixes a related gap: SDK identity reached HTTP requests but never the
WebSocket, so `ws_session_started` was anonymous even for an identified
caller. `RelayCast` now forwards it to the socket too.

The Python, Rust, and Swift SDKs still send only `agentRelayDistinctId`;
adding the user/org fields there is follow-up work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UEGpCHvkLVy7AhPgjrCbcG

* feat(telemetry): add actor_machine_id so machine cross-tabs survive login

Keying events on the user id answered "how many people" but destroyed "how
many machines": once a caller signs in, the distinct id IS the user id, so
the machine dimension disappeared exactly when it got interesting.

`X-Agent-Relay-Machine-Id` (and `agent_relay_machine_id` on a WS upgrade) is
now read into `actor_machine_id`, reported alongside `actor_user_id` rather
than instead of it. With `workspace_id` already on every server event, that
makes three questions answerable:

  - machines per workspace  — distinct actor_machine_id by workspace_id
  - accounts per machine    — distinct actor_user_id by actor_machine_id
  - machines per account    — distinct actor_machine_id by actor_user_id

The second is the interesting one: several machines on one workspace signed
into different cloud accounts is a shared team workspace; the same account
everywhere is one person on several hosts.

The machine id is also the distinct-id fallback for anonymous callers, so
not-logged-in traffic is still attributable to a host rather than collapsing
onto the workspace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UEGpCHvkLVy7AhPgjrCbcG

* fix(sdk,docs): forward identity to agent sockets; correct changelog and API docs

Addresses the three review findings on #300. All were valid.

**Agent sockets lost the new dimensions (P2).** `AgentClient.connect()` copied
only `agentRelayDistinctId` from its inherited `HttpClient`, so a
`/v1/node/ws` session reported `ws_session_started` as unauthenticated and
without user/machine/org — even though the same client's HTTP requests carried
them. Same class of bug as the `RelayCast` one this PR already fixed; I'd
missed the sibling.

Rather than adding the four fields to a third hand-copy, the root cause was
the duplication itself: three places (`withApiKey`, `RelayCast`, `AgentClient`)
each spread origin fields by hand, so any new dimension had to be remembered in
all three. Added `HttpClient.internalOrigin` as the single source and pointed
all three at it — a new dimension now reaches every socket at once.

The regression test asserts the actual agent socket URL, and I verified it goes
red against the old hand-copy before going green on the fix.

**Changelog claimed a shipped release (P1).** My rebase resolution put these
entries inside the dated `[6.2.0]` section — main's #299 had restructured the
file underneath me — falsely telling users the published package supports these
options, and leaving the release tooling nothing to carry forward. Moved to
`[Unreleased]`, raised Patch → Minor per the monotonic rule in AGENTS.md, and
added the package-level API detail to the engine and TypeScript SDK changelogs
that AGENTS.md requires.

**Undocumented API inputs (P1).** New accepted headers and WS query params were
in neither `README.md` nor `openapi.yaml`, so non-SDK callers had no documented
contract. Both now carry the full table, the charset/length rules, and why the
query-param forms exist.

Both docs were also stale independently: each described a `harness` option and
an `X-Relaycast-Harness` header that the engine no longer reads anywhere (it
was superseded by `X-Relaycast-Origin-Actor`). Corrected to the actual contract
while adding the identity fields.

Validation: turbo lint+build 17/17, engine 518 passed, SDK 414 passed,
openapi.yaml re-parsed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UEGpCHvkLVy7AhPgjrCbcG

* fix(engine,sdk): drop oversized identity values; validate each source in turn

**Oversized actor dimensions are now dropped, not truncated** (engine). A
truncated user or org id is a *different* id — it can collide with a real one
and attribute usage to the wrong person or company, so no attribution beats
wrong attribution. Only the four new actor dimensions changed;
`agent_relay_distinct_id` keeps its shipped cap-at-128 behaviour (covered by an
existing test on main), so `readIdentityValue` now takes an explicit
`onOversize` rather than hiding the difference. Every SDK caps well below these
limits, so only a malformed caller is affected.

**Identity source selection validates before it picks** (SDK). `pick` returned
the first non-empty candidate and sanitized afterwards, so a malformed
higher-priority value shadowed a valid lower-priority one and dropped the
dimension entirely — a wrapping host with a bad internal value silently lost
identity the caller had supplied correctly. Sanitizing inside the loop fixes
that and, as a side effect, computes each org value once instead of twice.

Both regressions are covered by tests verified red against the previous code.

Docs and changelogs updated to match: the length rule and its rationale in
`README.md` and `openapi.yaml`, and the entries condensed to the impact-first
form AGENTS.md asks for (dropping `HttpClient.internalOrigin`, which is
internal plumbing rather than user-facing API).

Not adopted, with reasons:
- Zod schemas for the length checks and the SDK sanitizers. `origin.ts` has no
  Zod dependency today and these run per request and per client construction;
  expressing "trim → charset → bound" as a schema is longer and slower for
  identical behaviour. `sanitizeAgentRelayDistinctId` is also a published
  export whose truncate-at-128 semantics must be preserved exactly, which
  `.max()` inverts.

Validation: turbo lint+build 17/17, engine 520 passed, SDK 416 passed,
openapi.yaml re-parsed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UEGpCHvkLVy7AhPgjrCbcG

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants