diff --git a/.agents/skills/cut-release/SKILL.md b/.agents/skills/cut-release/SKILL.md index 49ef2b0c3..7e3f75d9e 100644 --- a/.agents/skills/cut-release/SKILL.md +++ b/.agents/skills/cut-release/SKILL.md @@ -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 --- @@ -14,21 +14,27 @@ 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 --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. + + 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 @@ -36,6 +42,8 @@ The only human step is publishing the drafted release — Release Drafter has al ``` 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: @@ -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. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index fa8435c19..5d47dbbd3 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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 diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 814780d03..61347b2af 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -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: diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 78fe422d5..1c1dff5ad 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -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: diff --git a/AGENTS.md b/AGENTS.md index 5f60a3ded..217a5f976 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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: @@ -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///'`), and express any +divergence as an explicit `Pick`/`Omit`/`&` carrying the reason. + +**`Omit` 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` 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-`