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
23 changes: 23 additions & 0 deletions .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: PR title

# A merge to main is a release, and the (squash-)merge commit subject is the PR title — which
# semantic-release parses to choose the version bump. So the PR title must be a valid
# conventional commit (feat:, fix:, feat!:, chore:, ci:, docs: ...). This check enforces that,
# so a merge can't silently produce the wrong bump (or an accidental release).

on:
pull_request:
types: [opened, edited, synchronize]
branches: [main]

permissions:
pull-requests: read

jobs:
lint-title:
runs-on: ubuntu-latest
steps:
# Pin to a full commit SHA if you want stricter supply-chain guarantees.
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40 changes: 40 additions & 0 deletions .github/workflows/publish-manual.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Publish (manual)

# Manual escape hatch, for two cases:
# 1. First publish of a brand-new package (before it has a trusted publisher / has never been
# on the registry) — run this once the new package exists in the tree.
# 2. Recovery: an automated release published only some packages (registry hiccup). Re-run
# this with that release's version; publish-workspaces.mjs skips the ones already out.
#
# Pick the ref (branch/tag) to publish from in the "Run workflow" dialog; give the version to
# stamp. Idempotent — safe to re-run.

on:
workflow_dispatch:
inputs:
version:
description: "Version to stamp & publish (e.g. 0.2.0)"
required: true

permissions:
id-token: write # OIDC trusted publishing + npm provenance
contents: read

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "22.x"
registry-url: "https://registry.npmjs.org"

- name: Ensure npm supports trusted publishing
run: npm install -g npm@latest

- run: npm ci
- run: npm run build
- run: node scripts/stamp-version.mjs "${{ inputs.version }}"
- run: node scripts/publish-workspaces.mjs
66 changes: 66 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Release

# A merge to main IS a release (gated by your PR review). On every push to main this runs the
# full test gate; if it passes, semantic-release reads the conventional commits since the last
# vX.Y.Z tag, decides the bump, creates the tag + GitHub Release, stamps that one version across
# all workspaces (lockstep), and publishes every public package to npm with provenance via
# trusted publishing (OIDC — no NPM_TOKEN).
#
# No bump is committed back to main: the git tag + npm are the source of truth for the current
# version. Keep non-release work on feature/dev branches. If tests fail, semantic-release never
# runs, so a red main never publishes.

on:
push:
branches: [main]

# Never let two releases publish at once; don't cancel an in-flight publish.
concurrency:
group: release
cancel-in-progress: false

permissions:
contents: write # semantic-release pushes the tag + creates the GitHub Release
id-token: write # OIDC trusted publishing + npm provenance

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # semantic-release needs full history + tags to compute the bump

- uses: actions/setup-node@v4
with:
node-version: "22.x"
registry-url: "https://registry.npmjs.org"

# Trusted publishing (OIDC) needs npm >= 11.5.1; newer than what Node 22 bundles.
- name: Ensure npm supports trusted publishing
run: npm install -g npm@latest

# --- Test gate (mirrors ci.yml: Redis + NATS run for real, Matrix/XMPP self-skip) ---
- name: Start Redis
run: docker run -d --name parley-redis -p 6379:6379 redis:7-alpine
- name: Start NATS (JetStream)
run: docker run -d --name parley-nats -p 4222:4222 nats:2.10-alpine -js
- name: Wait for Redis + NATS to accept connections
shell: bash
run: |
for port in 6379 4222; do
for i in $(seq 1 30); do
(echo > "/dev/tcp/127.0.0.1/$port") >/dev/null 2>&1 && { echo "port $port up"; break; }
sleep 1
done
done

- run: npm ci
- run: npm run build
- run: npm test

# --- Release: bump decided from commits, tag + GitHub Release, lockstep npm publish ---
- name: semantic-release
run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
24 changes: 24 additions & 0 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"branches": ["main"],
"tagFormat": "v${version}",
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/exec",
{
"prepareCmd": "node scripts/stamp-version.mjs ${nextRelease.version}",
"publishCmd": "node scripts/publish-workspaces.mjs"
}
],
[
"@semantic-release/github",
{
"successComment": false,
"failComment": false,
"labels": false,
"releasedLabels": false
}
]
]
}
19 changes: 19 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,25 @@ it is plain MCP and can proceed immediately.

---

## Releases (automated)

Publishing is automated — full details in `CONTRIBUTING.md` ("Releases & versioning"); read it
before merging to `main`. In short:

- **A merge to `main` *is* a release.** CI runs the test gate, then `semantic-release` picks the
version bump, tags it, and publishes **all** packages to npm in lockstep with provenance
(trusted publishing / OIDC — no tokens). Never `npm publish` by hand or hand-edit `package.json`
versions; the git tag + npm are the source of truth. The one exception is the *first* publish of
a brand-new package (see `CONTRIBUTING.md`).
- **The bump comes from the PR title** (merges are squash-only; the title is the squash subject,
linted by `lint-title`). Conventional-commit prefix decides it: `feat:`→minor, `fix:`/`perf:`→
patch, `feat!:` or a `BREAKING CHANGE:` footer→major, `docs:`/`chore:`/`ci:`/`build:`/`refactor:`/
`test:`→no release.
- **Pre-1.0: avoid `!` / `BREAKING CHANGE`.** There is no special 0.x handling — a breaking change
jumps straight to `1.0.0`. Land early breaking changes as `feat:` until you deliberately cut 1.0.

---

## Testing discipline

- Each backend ships with tests that exercise the **same** seam contract (a shared conformance
Expand Down
72 changes: 72 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ XMPP) was added without touching either.
`examples/dev-compose/` instead), and a "Multiple concurrent sessions" section if the backend
has any shared-state gotchas (see [`examples/multi-session`](examples/multi-session/README.md)
for the pattern other backends follow).
7. **First publish (one-time).** A brand-new package has to be published by hand *once* before
automation can take over — npm trusted publishing can only be configured on a package that
already exists. See [Releases & versioning](#releases--versioning) for the exact steps. Every
release after that is automatic.

## Dev setup

Expand All @@ -83,8 +87,76 @@ conformance suites to actually run instead of skip.
## Before opening a PR

- `npm run build && npm test` green locally.
- **Title your PR as a [Conventional Commit].** A merge to `main` publishes, and the PR title is
the message that decides the version bump — see [Releases & versioning](#releases--versioning).
A CI check (`lint-title`) enforces this. Use `docs:`/`chore:`/`ci:` for changes that shouldn't
cut a release.
- Small, focused commits over one large one — each logical change is easier to review on its own.
(Branch commits are squashed on merge, so they're for reviewers, not the changelog.)
- If you touched a backend's `backend_config` shape, update that backend's README (the config
table + any shared/per-session notes) in the same PR.
- If you're not sure whether a change is a "seam change," ask first (open an issue) rather than
find out via review — see "The one rule that matters most" above.

## Releases & versioning

Releases are **fully automated**. A merge to `main` *is* a release — there's no separate publish
step, and you should never `npm publish` by hand or hand-edit a `package.json` version (the one
exception is the first publish of a brand-new package — see below).

How it works:

- On every push to `main`, CI runs the full test gate. If it's green,
[semantic-release](https://semantic-release.gitbook.io) reads the conventional-commit messages
since the last `vX.Y.Z` tag, decides the bump, creates the tag + a GitHub Release, stamps that
one version across **all** workspace packages (lockstep), and publishes every public package to
npm with provenance — via trusted publishing (OIDC), so there are no npm tokens anywhere.
Config lives in [`.releaserc.json`](.releaserc.json) and
[`.github/workflows/release.yml`](.github/workflows/release.yml).
- **No bump is committed back** to the tree. The git tag + npm are the source of truth for the
current version; `package.json` versions on `main` stay at the last-released number between
releases. Don't hand-edit them — the release stamps them in CI.

### The PR title is what releases

Merges are **squash-only**, and the PR title becomes the squash commit's subject (a `lint-title`
check enforces that it's a valid [Conventional Commit]). So the **PR title is authoritative** — your
branch's individual WIP commits are squashed away and don't affect the release. Prefix it:

| PR title prefix | Release | Example |
| --- | --- | --- |
| `feat:` | **minor** | `0.1.0 → 0.2.0` |
| `fix:` / `perf:` / `revert:` | **patch** | `0.1.0 → 0.1.1` |
| `feat!:` (any `type!:`, or a `BREAKING CHANGE:` footer in the PR body) | **major** | `0.1.0 → 1.0.0` |
| `docs:` `chore:` `ci:` `build:` `refactor:` `style:` `test:` | **none** | no release |

> **While Parley is pre-1.0, avoid `!` / `BREAKING CHANGE`.** semantic-release has no special 0.x
> handling — a breaking change jumps straight to `1.0.0`, not `0.2.0`. Land breaking-but-early
> changes as `feat:` until you deliberately mean to cut 1.0.

`main` is protected: PRs only, both CI checks green and up to date, linear history, squash-only.
Keep non-release work on feature/dev branches — a merge to `main` ships.

### First publish of a brand-new package (the one manual case)

npm trusted publishing can only be configured on a package that already exists, so a new backend
package must be published once by hand before automation can take over:

```bash
npm login # a @sharptrick publisher
npm run build
npm publish -w @sharptrick/parley-<name> --access public # no provenance on this bootstrap publish
```

Then add the trusted publisher on npmjs.com (the package → **Settings → Publishing access → Add
trusted publisher**: repo `sharpTrick/parley`, workflow `release.yml`). From the next release on,
it's automated like every other package.

### Recovering a partial release

If a release publishes only some packages (e.g. a registry hiccup mid-run), re-run the **Publish
(manual)** workflow (Actions tab → *Run workflow* → enter that release's version). It re-stamps and
publishes idempotently, skipping anything already on the registry
([`.github/workflows/publish-manual.yml`](.github/workflows/publish-manual.yml)).

[Conventional Commit]: https://www.conventionalcommits.org
Loading
Loading