Skip to content
Open
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
22 changes: 9 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,21 @@ jobs:
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
persist-credentials: false

- name: Setup Bun
uses: oven-sh/setup-bun@v2
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14

- name: Install dependencies
run: bun install --frozen-lockfile --ignore-scripts

- name: Check plugin manifest versions
run: bun run check:plugin-version

- name: Validate skills
run: bun run check:skills

Expand All @@ -38,14 +43,5 @@ jobs:

- name: Verify package contents
run: |
set -euo pipefail
PACKAGE_DIR="$RUNNER_TEMP/sim-skills-package"
PACKAGE_PATH="$RUNNER_TEMP/sim-skills.tgz"
bun pm pack --ignore-scripts --filename "$PACKAGE_PATH" --quiet
mkdir -p "$PACKAGE_DIR"
tar -xzf "$PACKAGE_PATH" -C "$PACKAGE_DIR"
test -x "$PACKAGE_DIR/package/dist/index.js"
test -f "$PACKAGE_DIR/package/.codex-plugin/plugin.json"
test -f "$PACKAGE_DIR/package/.claude-plugin/plugin.json"
test -f "$PACKAGE_DIR/package/skills/build-workflow/SKILL.md"
test -f "$PACKAGE_DIR/package/skills/sync-workspaces/SKILL.md"
bun pm pack --ignore-scripts --filename "$RUNNER_TEMP/sim-skills.tgz" --quiet
scripts/verify-package.sh "$RUNNER_TEMP/sim-skills.tgz"
159 changes: 159 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
name: Publish

on:
push:
branches: [main]
paths:
- 'package.json'
- 'bun.lock'
- 'src/**'
- 'skills/**'
- 'scripts/**'
- '.claude-plugin/**'
- '.codex-plugin/**'
- 'tsconfig*'
# A change to how the package is built or published has to republish it.
- '.github/workflows/publish.yml'

permissions:
contents: read

concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: false

jobs:
build:
name: build
runs-on: ubuntu-latest
timeout-minutes: 15
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
persist-credentials: false

- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14

- name: Install dependencies
run: bun install --frozen-lockfile --ignore-scripts

# Keeps package.json's version when it is ahead of npm (or nothing is
# published yet); otherwise takes the highest published patch plus one.
- name: Auto-bump package version
run: bun run release:version

# Both plugin manifests ship inside the tarball, so they must carry the
# version being published, not the committed one.
- name: Sync plugin manifest versions
run: bun run sync:plugin-version

- name: Read release version
id: version
run: echo "version=$(bun -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"

- name: Validate skills
run: bun run check:skills

- name: Run tests
run: bun run test

- name: Type-check package
run: bun run type-check

- name: Build package
run: bun run build

# The publish job ships this exact tarball, so what was verified is what
# ships.
- name: Pack package
run: bun pm pack --ignore-scripts --filename "$RUNNER_TEMP/package.tgz" --quiet

- name: Verify package contents
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
scripts/verify-package.sh "$RUNNER_TEMP/package.tgz"
for manifest in package/package.json package/.claude-plugin/plugin.json package/.codex-plugin/plugin.json; do
PACKED_VERSION="$(tar -xzOf "$RUNNER_TEMP/package.tgz" "$manifest" | bun -e 'console.log(JSON.parse(await Bun.stdin.text()).version)')"
if [ "$PACKED_VERSION" != "$VERSION" ]; then
echo "$manifest has version '$PACKED_VERSION', expected '$VERSION'." >&2
exit 1
fi
done

- name: Verify version is unpublished
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
if bun pm view "sim-skills@$VERSION" version > /dev/null 2>&1; then
echo "sim-skills@$VERSION is already published. The automatic version bump did not produce a unique release." >&2
exit 1
fi

- name: Upload package
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: package
path: ${{ runner.temp }}/package.tgz
if-no-files-found: error
retention-days: 1

# npm trusted publishing trades this job's OIDC token for a short-lived npm
# credential, so no npm token is stored anywhere. Only this job can mint one,
# and it runs no repository code: it publishes the tarball the build job
# verified. npm runs no lifecycle scripts for a tarball, and provenance is
# attached automatically.
publish:
name: publish
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
id-token: write
steps:
# Node 24 bundles npm 11.5.1+, which trusted publishing requires.
- name: Setup Node
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
with:
node-version: '24'

- name: Download package
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: package
path: ${{ runner.temp }}

- name: Publish to npm
run: npm publish "$RUNNER_TEMP/package.tgz" --access public --tag latest

- name: Summarize release
env:
VERSION: ${{ needs.build.outputs.version }}
run: echo "Published sim-skills@$VERSION with the 'latest' tag."

release:
name: release
needs: [build, publish]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
steps:
- name: Create tag and GitHub Release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: v${{ needs.build.outputs.version }}
run: |
if gh release view "$TAG" > /dev/null 2>&1; then
echo "Release $TAG already exists."
exit 0
fi
gh release create "$TAG" --target "$GITHUB_SHA" --title "$TAG" --generate-notes --latest
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ bunx skills add simstudioai/skills \
--yes
```

Once published, the npm package provides a shorter interactive installer:
The [`sim-skills`](https://www.npmjs.com/package/sim-skills) npm package wraps the same installer
with the Sim skills preselected:

```bash
bunx sim-skills
Expand Down Expand Up @@ -96,3 +97,11 @@ The skills assume the `sim` CLI is installed and authenticated. They never store

Workspace sync guidance requires a CLI and server exposing the v2 import-preview and fork sync
commands. The skill checks command availability before changing resources.

## Releasing

Every merge to `main` that changes the package publishes `sim-skills` to npm with the `latest` tag
and creates a matching `v<version>` GitHub Release. The publish workflow bumps the patch version
automatically, so a routine change needs no version edit. For a minor or major release, set the new
version in `package.json` and run `bun run sync:plugin-version` in the same pull request; CI fails if
the plugin manifests drift from `package.json`.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"test": "vitest run",
"check:skills": "bun run scripts/validate-skills.ts",
"sync:plugin-version": "bun run scripts/sync-plugin-version.ts",
"check:plugin-version": "bun run scripts/sync-plugin-version.ts --check",
"release:version": "bun run scripts/resolve-release-version.ts",
"prepublishOnly": "bun run check:skills && bun run test && bun run build"
},
"files": [
Expand Down
69 changes: 69 additions & 0 deletions scripts/resolve-release-version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, expect, it } from "vitest";
import {
fetchPublishedVersions,
resolveReleaseVersion,
} from "./resolve-release-version";

describe("resolveReleaseVersion", () => {
it("keeps the manifest version when nothing stable is published", () => {
expect(resolveReleaseVersion("0.1.1", [])).toBe("0.1.1");
expect(resolveReleaseVersion("0.1.1", ["0.1.0-preview.3"])).toBe("0.1.1");
});

it("keeps a manifest version ahead of the published line", () => {
expect(resolveReleaseVersion("0.3.0", ["0.2.9", "0.2.10"])).toBe("0.3.0");
});

it("bumps the highest published patch when the manifest is behind or equal", () => {
expect(resolveReleaseVersion("0.1.1", ["0.1.1", "0.2.4", "0.2.10"])).toBe(
"0.2.11",
);
expect(resolveReleaseVersion("0.2.10", ["0.2.10"])).toBe("0.2.11");
});

it("ignores prereleases above the stable line", () => {
expect(
resolveReleaseVersion("0.2.0", [
"0.2.0",
"0.3.0-preview.1",
"1.0.0-rc.1",
]),
).toBe("0.2.1");
});

it("rejects a non-stable manifest version", () => {
expect(() => resolveReleaseVersion("0.2.0-preview.1", [])).toThrow(
"stable X.Y.Z",
);
});
});

describe("fetchPublishedVersions", () => {
const respond = (status: number, body: unknown): typeof fetch =>
(async () =>
new Response(JSON.stringify(body), { status })) as typeof fetch;

it("treats a package npm has never seen as unpublished", async () => {
await expect(
fetchPublishedVersions(
"sim-skills",
respond(404, { error: "Not found" }),
),
).resolves.toEqual([]);
});

it("fails instead of guessing when the registry errors", async () => {
await expect(
fetchPublishedVersions("sim-skills", respond(503, {})),
).rejects.toThrow("503");
});

it("returns the published version keys", async () => {
await expect(
fetchPublishedVersions(
"sim-skills",
respond(200, { versions: { "0.1.1": {}, "0.1.2": {} } }),
),
).resolves.toEqual(["0.1.1", "0.1.2"]);
});
});
Loading
Loading