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
18 changes: 13 additions & 5 deletions .agents/skills/cut-release/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: cut-release
description: Runbook for cutting a release of @sistent/sistent to npm. Use when asked to "cut a release", "release Sistent", "publish a new version", or "ship the next @sistent/sistent". The flow is: wait for Release Drafter to fold the merged PR into the draft release, then publish that draft Release Drafter has already set the version and notes, and release.yml handles the npm publish (OIDC provenance) and the auto-merged version-bump-back.
description: Runbook for cutting a release of @sistent/sistent to npm. Use when asked to "cut a release", "release Sistent", "publish a new version", or "ship the next @sistent/sistent". The flow is: wait for Release Drafter to fold the merged PR into the draft release, then publish that draft - Release Drafter has already set the version (from the merged PR's labels) and the notes, and release.yml handles the npm publish (OIDC provenance) and the auto-merged version-bump-back.
user-invocable: true
---

Expand All @@ -14,28 +14,36 @@ Publishes a new version of the `@sistent/sistent` npm package. Releasing is **au

## The release chain (what actually happens)

1. **A PR merges to `master`** → `release-drafter.yml` runs and updates the **draft GitHub Release**: it folds that PR into the changelog and computes the next version from the PR's labels (major / minor / patch).
1. **A PR merges to `master`** → `release-drafter.yml` runs and updates the **draft GitHub Release**: it folds that PR into the changelog and computes the next version from the PR's labels (`major` / `minor` / `patch`, defaulting to `patch` when unlabelled).
2. **You publish the draft Release.** The tag (e.g. `v0.21.31`) becomes the version source of truth.
3. Publishing emits `release: published` → **`release.yml`**: sets `package.json#version` from the tag, `npm ci` → `npm run build` → `npm publish --provenance --access public`, then opens the `release/version-bump/vX.Y.Z` PR and **auto-merges it** so `master`'s `package.json`/`package-lock.json` track the published version without sitting idle.
4. `notify-dependents.yml` fires on the publish workflow's success and bumps downstream consumers (e.g. meshery, meshery-cloud) to the new version.

## Procedure

The only human step is publishing the drafted release Release Drafter has already done the versioning and notes.
The only human step is publishing the drafted release - Release Drafter has already done the versioning and notes.

1. **Wait for Release Drafter to fold your merged PR into the draft.** After the PR merges to `master`, `release-drafter.yml` runs (a few seconds). Confirm the draft now reflects the merged PR and shows the intended next version:
```bash
gh release list --repo layer5io/sistent # find the draft (isDraft = true)
gh release view <vX.Y.Z> --repo layer5io/sistent # confirm version + notes
```
If the version bump is wrong, it's driven by the merged PR's labels — relabel and let Release Drafter re-draft; do not hand-edit the tag.
If the version bump is wrong, it's driven by the merged PR's labels - relabel and let Release Drafter re-draft; do not hand-edit the tag. Which label a breaking change gets is the pre-1.0 rule in [`AGENTS.md`](../../../AGENTS.md)'s Releasing section.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Relabelling alone does **not** re-draft: `release-drafter.yml` runs on push to `master`, so nothing re-runs it after the merge. Re-run it by hand once the label is right:
```bash
gh workflow run release-drafter.yml --repo layer5io/sistent --ref master
```
Then re-check the draft with `gh release view` before publishing.

2. **Publish the draft Release. That's it.** No version editing, no notes, no tagging:
```bash
gh release edit <vX.Y.Z> --repo layer5io/sistent --draft=false --latest
```
Publishing triggers `release.yml`, which publishes to npm and auto-merges the version-bump-back PR.

> **A label-driven minor or major has never actually been drafted in this repo.** Until the config change alongside this note, the name and tag templates interpolated `$NEXT_PATCH_VERSION`, so `version-resolver` was inert and every `v0.21.*` release drafted as a patch regardless of labels (`v0.21.0` and the boundaries below it were created by hand). The first non-patch draft is worth confirming against step 1 before publishing rather than assuming.

## Verify

1. Watch the publish workflow to success:
Expand All @@ -52,7 +60,7 @@ The only human step is publishing the drafted release — Release Drafter has al
## What NOT to do

- ❌ Don't `npm publish` locally or `npm version`-tag by hand — the workflow owns publishing (with provenance) and the version comes from the release tag.
- ❌ Don't edit the draft's version or release notes Release Drafter owns both; fix PR labels instead.
- ❌ Don't edit the draft's version or release notes - Release Drafter owns both; fix PR labels instead.
- ❌ Don't publish expecting an unmerged PR to be included — the release ships `master`'s current state; merge first, then let Release Drafter update the draft.
- ❌ Don't republish an already-published npm version — npm publishes are effectively permanent; cut a new patch instead.

Expand Down
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ npm test -- src/__testing__/routing.test.ts --runInBand
npm test -- -t "routing utilities" --runInBand
```

Current CI runs the main checks on Node 20 and 22. Pull requests also trigger a downstream integration workflow that packs this library and installs it into `meshery/meshery`'s `ui/` app before building and testing there.
CI runs the main checks on the Node versions in the `node-version` matrix of `.github/workflows/node-checks.yml`; read them there rather than from a copy here that can drift. Pull requests also trigger a downstream integration workflow that packs this library and installs it into `meshery/meshery`'s `ui/` app before building and testing there.

## Known issues

Expand Down
8 changes: 6 additions & 2 deletions .github/release-drafter.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
name-template: 'Sistent v$NEXT_PATCH_VERSION'
tag-template: 'v$NEXT_PATCH_VERSION'
# $RESOLVED_VERSION, not $NEXT_PATCH_VERSION: it is what routes the
# version-resolver below into the drafted tag. With `default: patch` an
# unlabelled PR resolves to the next patch either way, so only a
# major/minor-labelled PR drafts differently than it used to.
name-template: 'Sistent v$RESOLVED_VERSION'
tag-template: 'v$RESOLVED_VERSION'
categories:
- title: '🚀 Features'
labels:
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/release-drafter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ on:
# our release branch
branches:
- master
# Manual re-draft. Release Drafter recomputes the draft from the merged PRs and
# their labels, so this is how a mislabelled release is corrected: relabel the
# merged PR, then re-run this workflow. Without it the drafter only ever runs on
# a push to master, so relabelling after the fact has no effect.
workflow_dispatch:

jobs:
update_release_draft:
Expand Down
74 changes: 72 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ Runbook: [`.agents/skills/cut-release/SKILL.md`](.agents/skills/cut-release/SKIL
Resolve "what is currently released" from the npm `latest` dist-tag and publish timestamps
(`npm view @sistent/sistent dist-tags time --json`), not by eyeballing semver order.

A consumer-visible breaking change - a published type that gains a required field, narrows, or
changes shape - is a **minor** here, not a major: sistent is pre-1.0, so `major` would assert 1.0
stability rather than describe the break. Label the PR `minor`, and note that the label is the only
signal that moves the version: [`.github/release-drafter.yml`](.github/release-drafter.yml) owns the
label-to-bump mapping and defaults to `patch`, so an unlabelled PR publishes a breaking change as a
patch.

Verify a published release **by content**, not by the version number moving. Two properties carry
the three-repo chain, and losing either fails downstream with errors that point nowhere near sistent:

Expand Down Expand Up @@ -146,6 +153,44 @@ sheet's human-readable category + function text, while the UUID is stable. Editi
typo fix, a plural made singular - renames the exported constant and orphans the old one. That is how
`1.3.35 -> 1.3.36` renamed 10 keys with every UUID unchanged, in a patch release.

## Wire shapes are derived from `@meshery/schemas`, never re-declared

Any type that is decoded from or encoded to a Meshery/Layer5 API is owned by `meshery/schemas`.
Sistent is upstream of every Meshery UI, so a shape hand-copied here propagates to all of them and a
rename upstream reaches consumers as a silent `undefined` rather than an error. Derive from the
canonical construct instead
(`import type { components } from '@meshery/schemas/constructs/<ver>/<c>/<C>'`), and express any
divergence as an explicit `Pick`/`Omit`/`&` carrying the reason.

**`Omit<T, 'gone'>` where `T` has no `gone` is a silent no-op**, so derivation alone does not survive
the rename it was adopted to catch: the omit stops removing anything and the override quietly becomes
an addition. [`src/__testing__/fixtures/schemaConstructAliases.ts`](src/__testing__/fixtures/schemaConstructAliases.ts)
closes that by asserting every omitted/narrowed key still exists upstream, and is the source of truth
for which local types are bound to which construct and why each divergence is kept. Read it before
adding or widening one. It is compiled by a `tsc` guard, not by `jest` - see the `tsc`-over-a-fixture
note under "Repo state that looks broken but is pre-existing".

When the canonical is the wrong one, keep the narrower local shape, link a filed `meshery/schemas`
issue from the type's doc comment, and verify the claim against the actual server struct before
filing - open examples: [#1142](https://github.com/meshery/schemas/issues/1142) (catalog data),
[#1143](https://github.com/meshery/schemas/issues/1143), [#1144](https://github.com/meshery/schemas/issues/1144)
(share/revoke), [#1145](https://github.com/meshery/schemas/issues/1145).

Such a workaround needs an expiry date, not just an issue link, or it outlives its upstream fix in
silence. Pair it with an `// @ts-expect-error` + `RequiresKey<Canonical, 'theMissingKey'>` entry in
the fixture: the suppression goes unused the moment the canonical gains the key, and tsc reports
TS2578. `TeamHasNoTeamId`, `EventResultHasNoAvatarUrl` and the two `CatalogDataHasNo*` entries are
the worked examples.

A wire mismatch here is not loud. meshery-cloud decodes request bodies with a strict `json.Unmarshal`
into `omitempty` structs and still answers 200, so a stale outbound key name is a successful no-op -
which is how sistent's share modal granted nothing for three months after the Phase 4 camelCase flip.
Outbound payload shapes therefore get their own module and a test pinning the literal key names, as
[`src/custom/ShareModal/resourceAccessPayload.ts`](src/custom/ShareModal/resourceAccessPayload.ts) does.
Those modules are root-exported so hosts stop hand-rolling the body, which makes them public API: the
validation that keeps an unusable value off the wire belongs in the builder, not only in the caller
that happens to render the error. A component-level guard is defence in depth on top of it.

## `disabled` on a MUI `MenuItem` does not stop a click

MUI enforces `disabled` on non-`<button>` elements (a `MenuItem` renders `<li>`) purely with
Expand All @@ -169,6 +214,27 @@ a new public component or type in a `src/<domain>/` subtree, also add an explici
of examples there, e.g. `FeedbackButton`, `NavigationItem`). Verify by building and grepping
`dist/index.d.ts` for the symbol - a green `jest`/lint run will not catch this.

The explicit block is a stopgap, not the fix. Measure the real gap before assuming a symbol is
covered - it is large, and every uncovered symbol is one a consumer must shim locally:

```bash
npm run build
node -e 'const f=require("fs"),names=t=>{const s=new Set();
for(const b of t.matchAll(/export\s*\{([^{}]*)\}\s*;?/g))
b[1].split(",").map(x=>x.trim()).filter(Boolean)
.forEach(x=>s.add(x.replace(/^type\s+/,"").split(/\s+as\s+/).pop().trim()));
return s;};
const rt=names(f.readFileSync("dist/index.mjs","utf8"));
const dt=names(f.readFileSync("dist/index.d.ts","utf8"));
console.log([...rt].filter(n=>/^[A-Za-z_$][\w$]*$/.test(n)&&!dt.has(n)).sort().join("\n"))'
```

As of this change that reports 130 of 729 runtime exports absent from the declaration bundle -
`WorkspaceCard`, `TeamTable`, `UsersTable`, `CustomImage`, `ErrorBoundary` and most of
`src/custom/` among them. Adding 130 lines is not the answer; the durable fix is in how the
declaration bundle is produced. Until then, prefer extending this list over leaving a symbol
uncovered, and do not read its absence as "that component is intentionally private".

## Repo state that looks broken but is pre-existing

`prettier --check` fails on dozens of files and `tsc --noEmit` reports errors across `src/`
Expand All @@ -180,8 +246,12 @@ A type contract can still be gated, just not by a type-only assertion file: jest
`@swc/jest`, which strips types without checking them, so such a file passes no matter what it
asserts. Shell out to `tsc` over a scoped fixture, assert the fixture is in the compiled program
(`--listFiles`) before trusting an empty diagnostic list, then filter the diagnostics to that
fixture - [`src/__testing__/navigationItemTitleTypes.test.ts`](src/__testing__/navigationItemTitleTypes.test.ts)
is the worked pattern, including the checks that keep the filter from turning the guard vacuous.
fixture. [`src/__testing__/helpers/tscFixture.ts`](src/__testing__/helpers/tscFixture.ts) is that
harness, and documents each check that keeps the filter from turning the guard vacuous - call
`typeCheckFixture(fixture, project)` from a new guard rather than copying it, because a fix to one
copy silently leaves the other unguarded. `navigationItemTitleTypes.test.ts` and
`schemaConstructAliasTypes.test.ts` are the two worked callers; both assert "compiles the fixture"
before the emptiness assertions, and the ordering is load-bearing.

## Every commit needs a sign-off matching its own author

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ Before you begin, ensure you have the following installed on your system:

### How to get started

Make sure you have `npm` on `node@16` and above. The CI workflows checks for in `node@16`, `node@18` and `node@20`
Develop on a Node version that CI actually tests. The tested versions are the `node-version` matrix
in [`.github/workflows/node-checks.yml`](.github/workflows/node-checks.yml) - read them there rather
than from a copy that can drift.

<div>&nbsp;</div>

Expand Down
Loading
Loading