diff --git a/.github/workflows/publish-sdk.yml b/.github/workflows/publish-sdk.yml index cebbd013b..d5f51500c 100644 --- a/.github/workflows/publish-sdk.yml +++ b/.github/workflows/publish-sdk.yml @@ -13,7 +13,7 @@ on: - Dry run permissions: - contents: read + contents: write id-token: write jobs: @@ -64,3 +64,21 @@ jobs: else npm publish --workspace @termix-ssh/plugin-sdk --provenance --access public fi + + - name: Tag and release + if: ${{ inputs.mode == 'Publish' }} + env: + GH_TOKEN: ${{ github.token }} + run: | + VERSION=$(node -p "require('./packages/plugin-sdk/package.json').version") + node -e " + const fs = require('fs'); + const { changelogSection } = require('./packages/plugin-sdk/dist/changelog.js'); + const notes = changelogSection(fs.readFileSync('packages/plugin-sdk/CHANGELOG.md', 'utf8'), process.argv[1]); + fs.writeFileSync('sdk-notes.md', notes); + " "$VERSION" + gh release create "sdk-v$VERSION" \ + --target "${{ github.sha }}" \ + --title "Plugin SDK $VERSION" \ + --notes-file sdk-notes.md \ + --latest=false diff --git a/package-lock.json b/package-lock.json index 39e2ea9f8..a7f402f21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15049,7 +15049,7 @@ }, "packages/plugin-sdk": { "name": "@termix-ssh/plugin-sdk", - "version": "1.0.1", + "version": "1.0.2", "license": "Apache-2.0", "dependencies": { "@tailwindcss/node": "^4.2.4", diff --git a/packages/plugin-sdk/CHANGELOG.md b/packages/plugin-sdk/CHANGELOG.md new file mode 100644 index 000000000..8daec323e --- /dev/null +++ b/packages/plugin-sdk/CHANGELOG.md @@ -0,0 +1,54 @@ +# Changelog + +## 1.0.2 + +### Added + +- `features` manifest field: up to 20 short lines about what the plugin does, shown on its page. `pluginFeatures()` reads them + +### Changed + +- `parseChangelog()` and `validateChangelog()` flag a `## Unreleased` section. Notes go under the version they ship in + +### Removed + +- `Changelog.unreleased` + +## 1.0.1 + +### Added + +- Plugin API 1.1 +- `app.registerOnboardingStep()` adds a step to onboarding, with `OnboardingStepProps` and `OnboardingStepContribution`. Needs `engine.api` 1.1 +- `video` manifest field: a YouTube link shown at the top of the plugin's page, read with `youtubeVideoId()` +- `@termix-ssh/plugin-sdk/changelog` parses and checks a plugin's `CHANGELOG.md` +- `termix-plugin changelog` prints one version's notes from `CHANGELOG.md` +- `termix-plugin dev` installs a plugin on a running server and reinstalls it on every change. Needs developer mode and an admin API key +- `renderOnboardingStep()` and `registered.onboardingSteps()` in `renderWithApp()` +- `AddButton`, `FormFooter`, `ListRow`, `ListRowAction`, `ListRowFolder`, `ListBadge` and `PanelList` in `@termix-ssh/plugin-sdk/ui` +- `PanelSearch` takes `disabled`, `title`, `onEnter` and `inputRef`, and `SurfaceScope` takes `onClose` + +### Changed + +- Release notes moved from `CHANGELOG.json` to `CHANGELOG.md`, which `pack` now ships. `validate` flags a leftover `CHANGELOG.json` +- `ConnectionScreen` dropped `errorMessage` and `retryLabel` and gained `onClose`. `ConnectionLogPanel` dropped `position` +- Plugin tests run in worker threads, since forked workers crash now and then on Windows +- The test setup stubs `ResizeObserver` for Radix controls under jsdom +- `onboarding` is a reserved plugin id + +### Deprecated + +- The `onboarding.*` action slots. Use `registerOnboardingStep()` + +### Removed + +- `schema/changelog.schema.json` + +## 1.0.0 + +### Added + +- Backend, frontend, database, manifest, capability and settings types for plugin API 1.0 +- Testing helpers and a Vitest preset for plugin tests +- The `@termix-ssh/plugin-sdk/ui` component set +- The `termix-plugin` CLI to build, validate, test, pack, sign and write migrations diff --git a/packages/plugin-sdk/package.json b/packages/plugin-sdk/package.json index 4e9c7c848..430cfb359 100644 --- a/packages/plugin-sdk/package.json +++ b/packages/plugin-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@termix-ssh/plugin-sdk", - "version": "1.0.1", + "version": "1.0.2", "description": "The contract between Termix core and a plugin: types, the capability catalog and the manifest schema.", "license": "Apache-2.0", "repository": { @@ -94,6 +94,7 @@ "schema", "tsconfig.plugin.json", "README.md", + "CHANGELOG.md", "client.d.ts" ], "scripts": { diff --git a/packages/plugin-sdk/src/changelog.ts b/packages/plugin-sdk/src/changelog.ts index 3597127ee..446baed42 100644 --- a/packages/plugin-sdk/src/changelog.ts +++ b/packages/plugin-sdk/src/changelog.ts @@ -3,12 +3,7 @@ * * # Changelog * - * ## Unreleased - * - * ### Added - * - Something merged but not shipped yet - * - * ## 1.1.0 - 2026-10-06 + * ## 1.1.0 * * An optional summary paragraph. * @@ -19,7 +14,8 @@ * - Something broken * * Releases are listed newest first, and the newest one has to be the - * manifest version. The date is optional until the version ships. + * manifest version. Notes go straight under the version they ship in, there + * is no Unreleased section. */ import semver from "semver"; @@ -48,7 +44,6 @@ export interface ChangelogRelease { } export interface Changelog { - unreleased: ChangelogChange[]; /** Newest first, as written. */ releases: ChangelogRelease[]; } @@ -135,7 +130,7 @@ export function parseChangelog(markdown: string): { problems: string[]; } { const problems: string[] = []; - const changelog: Changelog = { unreleased: [], releases: [] }; + const changelog: Changelog = { releases: [] }; const lines = markdown.replace(/\r/g, "").split("\n"); type Block = { heading: string; line: number; body: string[] }; @@ -151,12 +146,11 @@ export function parseChangelog(markdown: string): { }); const seen = new Set(); - blocks.forEach((block, i) => { + blocks.forEach((block) => { if (UNRELEASED_HEADING.test(block.heading)) { - if (i !== 0) problems.push(`"## Unreleased" has to come first`); - const notes = parseReleaseNotes(block.body.join("\n"), "Unreleased"); - problems.push(...notes.problems); - changelog.unreleased = notes.changes; + problems.push( + `line ${block.line}: drop "## Unreleased", notes go under the version they ship in`, + ); return; } diff --git a/scripts/check-changelog.cjs b/scripts/check-changelog.cjs index 3caf88211..063e378fa 100644 --- a/scripts/check-changelog.cjs +++ b/scripts/check-changelog.cjs @@ -9,11 +9,23 @@ if (!fs.existsSync(sdk)) { process.exit(1); } -const { parseChangelog } = require(sdk); -const { problems } = parseChangelog( - fs.readFileSync(path.join(root, "CHANGELOG.md"), "utf8"), +const { parseChangelog, validateChangelog } = require(sdk); +const read = (file) => fs.readFileSync(path.join(root, file), "utf8"); + +const problems = parseChangelog(read("CHANGELOG.md")).problems.map( + (problem) => `CHANGELOG.md: ${problem}`, ); + +// The SDK is published on its own, so its newest release has to be the package version. +const sdkVersion = JSON.parse(read("packages/plugin-sdk/package.json")).version; +problems.push( + ...validateChangelog( + read("packages/plugin-sdk/CHANGELOG.md"), + sdkVersion, + ).map((problem) => `packages/plugin-sdk/CHANGELOG.md: ${problem}`), +); + if (problems.length > 0) { - for (const problem of problems) console.error(` CHANGELOG.md: ${problem}`); + for (const problem of problems) console.error(` ${problem}`); process.exit(1); } diff --git a/scripts/plugin-changelog.test.ts b/scripts/plugin-changelog.test.ts index 3b163a9b5..34518c43c 100644 --- a/scripts/plugin-changelog.test.ts +++ b/scripts/plugin-changelog.test.ts @@ -9,11 +9,6 @@ import { const FILE = [ "# Changelog", "", - "## Unreleased", - "", - "### Fixed", - "- A bug not shipped yet", - "", "## 1.1.0 - 2026-10-06", "", "Second release.", @@ -34,12 +29,9 @@ const FILE = [ ].join("\n"); describe("parseChangelog", () => { - it("reads releases, summaries, sections and unreleased changes", () => { + it("reads releases, summaries and sections", () => { const { changelog, problems } = parseChangelog(FILE); expect(problems).toEqual([]); - expect(changelog.unreleased).toEqual([ - { type: "fixed", text: "A bug not shipped yet" }, - ]); expect(changelog.releases).toEqual([ { version: "1.1.0", @@ -104,12 +96,19 @@ describe("parseChangelog", () => { ); }); - it("flags a release with no changes and Unreleased out of place", () => { - const { problems } = parseChangelog( - "## 1.0.0\n\nJust a summary.\n\n## Unreleased\n### Added\n- x\n", - ); + it("flags a release with no changes", () => { + const { problems } = parseChangelog("## 1.0.0\n\nJust a summary.\n"); expect(problems).toContain("1.0.0 has no changes listed"); - expect(problems).toContain('"## Unreleased" has to come first'); + }); + + it("flags an Unreleased section", () => { + const { changelog, problems } = parseChangelog( + "## Unreleased\n### Added\n- x\n\n## 1.0.0\n### Added\n- y\n", + ); + expect(problems).toEqual([ + 'line 1: drop "## Unreleased", notes go under the version they ship in', + ]); + expect(changelog.releases.map((r) => r.version)).toEqual(["1.0.0"]); }); it("ignores headings inside code fences", () => {