diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c20856b..8d04d39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,6 +70,7 @@ jobs: run: | node --test npm/clawscan/test/*.test.mjs node --test scripts/build-npm-package.test.mjs + node --test scripts/release-notes.test.mjs node scripts/build-npm-package.mjs --version v0.0.0 --pack --smoke - name: Test runtime update branch push diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c7b65ae..4fffbd0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,9 +80,14 @@ jobs: publish: name: Publish GitHub Release runs-on: ubuntu-latest - needs: build + needs: [build, publish-npm] if: github.event_name == 'push' || inputs.publish steps: + - name: Checkout release notes + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.build.outputs.checkout_ref }} + - name: Download artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -94,10 +99,12 @@ jobs: GH_TOKEN: ${{ github.token }} RELEASE_TAG: ${{ needs.build.outputs.version }} run: | + npm view "@openclaw/clawscan@${RELEASE_TAG#v}" --json > npm-metadata.json + node scripts/release-notes.mjs "$RELEASE_TAG" npm-metadata.json > release-notes.md gh release create "$RELEASE_TAG" dist/* \ --repo "$GITHUB_REPOSITORY" \ --title "$RELEASE_TAG" \ - --generate-notes + --notes-file release-notes.md publish-npm: name: Publish ClawScan npm package diff --git a/CHANGELOG.md b/CHANGELOG.md index 13e40d1..c5c9190 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,15 @@ ## Unreleased -- Prevent large Hugging Face `Retry-After` values from overflowing into immediate retries while preserving server cooldowns and cancellation. -- Label worker-owned Docker containers with optional run and command IDs so supervisors can clean up after cancellation. Thanks @jesse-merhi (#55). -- Add `--platform` to build smaller npm tarballs for one supported operating system and architecture while retaining the default universal package. -- Preserve every scanner report when sanitized target, profile, or custom scanner names collide with each other or generated numeric suffixes. +## 0.2.0 - 2026-09-22 + +**Highlights:** Preserve scanner evidence, bound benchmark input memory, and identify worker-owned containers for cleanup. + +- Preserve every scanner report when sanitized target, profile, or custom scanner names collide with each other or generated numeric suffixes (#54). - Fix unbounded memory use when loading benchmark `--ids` from files or HTTP, including whitespace-padded IDs; document selection limits and preserve full-set JSONL support. Thanks @SebTardif (#47). -- Refresh bundled scanner tools and Go dependencies; source builds now require Go 1.27.1, and the Docker runtime uses Node.js 24 LTS. +- Label worker-owned Docker containers with optional run and command IDs so supervisors can clean up after cancellation. Thanks @jesse-merhi (#55). +- Prevent large Hugging Face `Retry-After` values from overflowing into immediate retries while preserving server cooldowns and cancellation (#58). +- Add `--platform` to build smaller npm tarballs for one supported operating system and architecture while retaining the default universal package. Thanks @vincentkoc (#56). +- Repair contributor links and add the missing scanner-adapter guide. Thanks @atarico for the documentation report (#57). +- Refresh bundled scanner tools and Go dependencies; source builds now require Go 1.27.1, and the Docker runtime uses Node.js 24 LTS (#59). +- Publish changelog-backed release notes with verified npm metadata and show live CI and release status badges. diff --git a/README.md b/README.md index 05f7a1f..4d0ffde 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ ClawScan is a composable security scanning harness for agent skills. Run a suite of skill security scanners, pass the results to a judge harness, and compare against multiple skill security benchmarks. -[![CI](https://img.shields.io/badge/CI-passing-brightgreen)](https://github.com/openclaw/clawscan/actions/workflows/ci.yml?query=branch%3Amain) -[![Release](https://img.shields.io/badge/Release-passing-brightgreen)](https://github.com/openclaw/clawscan/actions/workflows/release.yml) -[![Latest release](https://img.shields.io/badge/latest%20release-unreleased-lightgrey)](https://github.com/openclaw/clawscan/releases) +[![CI](https://github.com/openclaw/clawscan/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/openclaw/clawscan/actions/workflows/ci.yml?query=branch%3Amain) +[![Release](https://github.com/openclaw/clawscan/actions/workflows/release.yml/badge.svg)](https://github.com/openclaw/clawscan/actions/workflows/release.yml) +[![Latest release](https://img.shields.io/github/v/release/openclaw/clawscan)](https://github.com/openclaw/clawscan/releases) ## Quick Start diff --git a/scripts/release-notes.mjs b/scripts/release-notes.mjs new file mode 100644 index 0000000..252683b --- /dev/null +++ b/scripts/release-notes.mjs @@ -0,0 +1,33 @@ +import { readFileSync } from "node:fs"; +import { pathToFileURL } from "node:url"; +import { normalizePackageVersion } from "./build-npm-package.mjs"; + +export function releaseNotes(changelog, tag, metadata) { + const version = normalizePackageVersion(tag); + const lines = changelog.split("\n"); + const start = lines.findIndex((line) => line.startsWith(`## ${version} - `)); + if (start === -1) throw new Error(`Missing changelog section for ${version}`); + const end = lines.findIndex((line, index) => index > start && line.startsWith("## ")); + const body = lines.slice(start + 1, end === -1 ? undefined : end).join("\n").trim(); + if (!body) throw new Error(`Empty changelog section for ${version}`); + if (metadata.name !== "@openclaw/clawscan" || metadata.version !== version) { + throw new Error("npm metadata does not match the release"); + } + const tarball = metadata.dist?.tarball; + const integrity = metadata.dist?.integrity; + if (tarball !== `https://registry.npmjs.org/@openclaw/clawscan/-/clawscan-${version}.tgz` || + !/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(integrity ?? "")) { + throw new Error("npm metadata is missing a valid registry tarball or integrity"); + } + return `${body}\n\n## Package\n\n` + + `[npm ${version}](https://www.npmjs.com/package/@openclaw/clawscan/v/${version}) ยท ` + + `[Registry tarball](${tarball})\n\nIntegrity: \`${integrity}\`\n`; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + process.stdout.write(releaseNotes( + readFileSync("CHANGELOG.md", "utf8"), + process.argv[2], + JSON.parse(readFileSync(process.argv[3], "utf8")), + )); +} diff --git a/scripts/release-notes.test.mjs b/scripts/release-notes.test.mjs new file mode 100644 index 0000000..ae36aac --- /dev/null +++ b/scripts/release-notes.test.mjs @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { releaseNotes } from "./release-notes.mjs"; + +const metadata = { + name: "@openclaw/clawscan", + version: "0.2.0", + dist: { + tarball: "https://registry.npmjs.org/@openclaw/clawscan/-/clawscan-0.2.0.tgz", + integrity: "sha512-dGVzdA==", + }, +}; +const changelog = "# Changelog\n\n## Unreleased\n\n## 0.2.0 - 2026-09-22\n\n**Highlights:** Changes.\n\n- Fixed.\n\n## 0.1.8 - 2026-09-10\n\n- Older.\n"; + +test("publishes only the requested changelog section and verified package metadata", () => { + const notes = releaseNotes(changelog, "v0.2.0", metadata); + assert.ok(notes.startsWith("**Highlights:** Changes.\n\n- Fixed.\n\n## Package")); + assert.ok(!notes.includes("Older") && !notes.includes("Unreleased")); + assert.ok(notes.includes(metadata.dist.tarball)); + assert.ok(notes.includes(metadata.dist.integrity)); + assert.equal(releaseNotes(changelog.split("## 0.1.8")[0], "v0.2.0", metadata), notes); +}); + +test("refuses missing or empty notes and mismatched registry metadata", () => { + assert.throws(() => releaseNotes("## Unreleased", "v0.2.0", metadata), /Missing/); + assert.throws(() => releaseNotes("## 0.2.0 - 2026-09-22\n", "v0.2.0", metadata), /Empty/); + assert.throws(() => releaseNotes(changelog, "v0.2.0", { ...metadata, version: "0.1.8" }), /match/); + assert.throws(() => releaseNotes(changelog, "v0.2.0", { ...metadata, dist: {} }), /tarball/); +});