diff --git a/.claude/commands/do-release.md b/.claude/commands/do-release.md new file mode 100644 index 0000000..c62117a --- /dev/null +++ b/.claude/commands/do-release.md @@ -0,0 +1,135 @@ +--- +name: do-release +description: Cut a new release of all @learnkit-ai packages. Triggers the GitHub Actions release pipeline - bumps versions across all packages, opens a bump PR, then tags and publishes to npm after merge. +--- + +# Release @learnkit-ai packages + +Run this whenever you want to cut a new version. Walks through every step and confirms each one. + +## Step 1 - Determine bump type + +If the user did not pass an argument, ask: + +> What kind of release is this? +> +> - `patch` - bug fixes only (0.1.1 -> 0.1.2) +> - `minor` - new features, backward-compatible (0.1.1 -> 0.2.0) +> - `major` - breaking changes (0.1.1 -> 1.0.0) + +Use the argument directly if provided: `/do-release patch`, `/do-release minor`, `/do-release major`. + +## Step 2 - Verify you are on main and clean + +```bash +git checkout main && git pull +git status +``` + +If there are uncommitted changes, stop and tell the user to commit or stash them first. + +## Step 3 - Confirm current versions + +```bash +node -p "require('./packages/core/package.json').version" +``` + +Show the user: "Current version is X.Y.Z - this will bump to A.B.C across all packages (schemas, core, react, cli). Proceed?" + +Wait for confirmation before continuing. + +## Step 4 - Trigger the Release workflow + +```bash +gh workflow run release.yml --field bump= +``` + +Then immediately check the run started: + +```bash +sleep 3 && gh run list --workflow=release.yml --limit=1 +``` + +## Step 5 - Watch the Release workflow + +```bash +gh run watch +``` + +The workflow will: +- Run typecheck and tests across all packages +- Bump version in all 4 package.json files (schemas, core, react, cli) +- Push branch `chore/bump-vX.Y.Z` +- Open a PR automatically + +If the workflow fails at "Push branch and open PR" with a `createPullRequest` permission error: +- The branch was still pushed - create the PR manually: + ```bash + gh pr create --title "Bump version to X.Y.Z" \ + --body "Automated version bump. Merging triggers tag creation and npm publish." \ + --base main --head chore/bump-vX.Y.Z + ``` +- Ask user to enable: Settings -> Actions -> General -> "Allow GitHub Actions to create and approve pull requests" + +## Step 6 - Show the bump PR + +```bash +gh pr list --head chore/bump-v --limit=1 +``` + +Show the PR URL and tell the user: + +> PR is open. Once CI passes, merge it to trigger the tag and npm publish automatically. + +## Step 7 - After the user merges the bump PR + +Pull main and verify: + +```bash +git checkout main && git pull +node -p "require('./packages/core/package.json').version" +``` + +Watch `tag-on-merge.yml` fire: + +```bash +gh run list --workflow=tag-on-merge.yml --limit=1 +gh run watch +``` + +## Step 8 - Watch npm publish + +Once `tag-on-merge.yml` creates the release, `publish.yml` fires automatically: + +```bash +gh run list --workflow=publish.yml --limit=1 +gh run watch +``` + +## Step 9 - Confirm release is live + +```bash +npm show @learnkit-ai/core version +gh release view vX.Y.Z +``` + +Tell the user: + +> All packages published to npm at vX.Y.Z: +> - `@learnkit-ai/schemas@X.Y.Z` +> - `@learnkit-ai/core@X.Y.Z` +> - `@learnkit-ai/react@X.Y.Z` +> - `@learnkit-ai/cli@X.Y.Z` +> +> Release: https://github.com/learnkit-ai/learnkit/releases/tag/vX.Y.Z + +--- + +## Known gotchas + +| Problem | Cause | Fix | +|---------|-------|-----| +| `createPullRequest` error in workflow | Repo setting not enabled | Settings -> Actions -> General -> enable "Allow GitHub Actions to create and approve pull requests" | +| `tag-on-merge` skips tag creation | Merge commit message doesn't contain `chore/bump-v` | Check that the bump branch was named `chore/bump-vX.Y.Z` | +| `publish.yml` fails with 401 | NPM_TOKEN secret expired | Run `gh secret set NPM_TOKEN --body NEW_TOKEN --repo learnkit-ai/learnkit` | +| Orphaned tag pointing to wrong commit | Cancelled push | `git push origin --delete refs/tags/vX.Y.Z` then re-run | diff --git a/.claude/commands/update-website.md b/.claude/commands/update-website.md new file mode 100644 index 0000000..7e6eb3f --- /dev/null +++ b/.claude/commands/update-website.md @@ -0,0 +1,88 @@ +--- +name: update-website +description: Sync apps/web with the latest package versions - update install snippets, version numbers in docs, and any copy that references the CLI or packages. +--- + +# Update learnkit-ai.com + +Keeps `apps/web` in sync with the current state of the packages. +Run this after every release to make sure the site reflects what is actually shipped. + +## Step 1 - Read the current version + +```bash +node -p "require('./packages/core/package.json').version" +``` + +Note the version. All install snippets and version references on the site should match. + +## Step 2 - Check what changed since last release + +```bash +git log --oneline $(git describe --tags --abbrev=0 HEAD^)..HEAD +``` + +Skim the log for: +- New roles or tools added -> update DemoFlow options +- New API surface -> update /developers page and code examples +- Breaking changes -> update install/getting-started copy +- Bug fixes worth calling out -> consider a blog post + +## Step 3 - Update install snippets + +Search for any hardcoded version numbers or install commands: + +```bash +grep -r "@learnkit-ai" apps/web/src --include="*.tsx" --include="*.ts" --include="*.mdx" -l +``` + +For each file found, update: +- `pnpm add @learnkit-ai/core @learnkit-ai/react` (no version pin needed - latest is fine) +- Any hardcoded version strings like `@0.1.1` -> `@X.Y.Z` + +## Step 4 - Update the developers page + +File: `apps/web/src/app/developers/page.tsx` (or `components/developers/`) + +Check: +- Code block in `CodeBlock.tsx` - does the import example match current API? +- `DeveloperHero.tsx` - version badge shows correct version + +## Step 5 - Update roles and tools if new ones shipped + +Source of truth: +```bash +node -e "const {getSupportedRoles, getSupportedTools} = require('./packages/core/dist/index.cjs'); console.log(getSupportedRoles()); console.log(getSupportedTools());" +``` + +Check against `apps/web/src/components/demo/DemoFlow.tsx` - the role/tool dropdowns should list all supported values. + +## Step 6 - Verify the build + +```bash +pnpm --filter @learnkit-ai/web build 2>&1 | tail -20 +``` + +Fix any TypeScript or missing import errors before committing. + +## Step 7 - Commit and push via PR + +```bash +git checkout -b chore/sync-website-vX.Y.Z +git add apps/web/src/ +git commit -m "Sync website with vX.Y.Z" +git push -u origin chore/sync-website-vX.Y.Z +gh pr create --title "Sync website with vX.Y.Z" --body "Updates install snippets, version references, and any copy that changed in this release." +``` + +--- + +## Quick reference - what lives where + +| What | Source of truth | Website file | +|------|----------------|--------------| +| Supported roles | `packages/core/src/roles.ts` | `apps/web/src/components/demo/DemoFlow.tsx` | +| Supported tools | `packages/core/src/tools.ts` | `apps/web/src/components/demo/DemoFlow.tsx` | +| Install command | `packages/core/package.json` version | `apps/web/src/components/developers/CodeBlock.tsx` | +| API surface | `packages/core/src/index.ts` | `apps/web/src/app/developers/page.tsx` | +| React API | `packages/react/src/index.ts` | `apps/web/src/components/developers/CodeBlock.tsx` | diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..1f3c85d --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,60 @@ +name: Publish to npm + +# Step 3 of 3: fires when a GitHub Release is published (triggered by tag-on-merge.yml). +# Publishes @learnkit-ai/schemas, @learnkit-ai/core, @learnkit-ai/react, @learnkit-ai/cli. + +on: + release: + types: [published] + workflow_dispatch: + inputs: + ref: + description: 'Tag or branch to publish (e.g. v0.1.2)' + required: true + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name || inputs.ref }} + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + registry-url: https://registry.npmjs.org + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build all packages + run: | + pnpm --filter @learnkit-ai/schemas build + pnpm --filter @learnkit-ai/core build + pnpm --filter @learnkit-ai/react build + pnpm --filter @learnkit-ai/cli build + + - name: Run checks + run: | + pnpm --filter @learnkit-ai/schemas typecheck + pnpm --filter @learnkit-ai/core typecheck + pnpm --filter @learnkit-ai/react typecheck + pnpm --filter @learnkit-ai/schemas test + pnpm --filter @learnkit-ai/core test + pnpm --filter @learnkit-ai/react test + + - name: Publish packages (schemas first, then dependents) + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + pnpm --filter @learnkit-ai/schemas publish --no-git-checks --access public + pnpm --filter @learnkit-ai/core publish --no-git-checks --access public + pnpm --filter @learnkit-ai/react publish --no-git-checks --access public + pnpm --filter @learnkit-ai/cli publish --no-git-checks --access public diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6fdf85f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,94 @@ +name: Release + +# Step 1 of 3: bump all package versions on a branch and open a PR. +# Step 2 is tag-on-merge.yml — creates the git tag + GitHub Release when this PR lands. +# Step 3 is publish.yml — publishes all packages to npm when the release is created. + +on: + workflow_dispatch: + inputs: + bump: + description: 'Version bump type' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + - major + +jobs: + bump: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run CI checks + run: | + pnpm --filter @learnkit-ai/schemas typecheck + pnpm --filter @learnkit-ai/core typecheck + pnpm --filter @learnkit-ai/react typecheck + pnpm --filter @learnkit-ai/cli typecheck + pnpm --filter @learnkit-ai/schemas test + pnpm --filter @learnkit-ai/core test + pnpm --filter @learnkit-ai/react test + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Bump all package versions + id: bump + run: | + OLD=$(node -p "require('./packages/core/package.json').version") + node -e " + const fs = require('fs'); + const semver = require('./node_modules/.pnpm/semver@*/node_modules/semver') || { inc: (v,t) => { const p=v.split('.').map(Number); if('${{ inputs.bump }}'==='major'){p[0]++;p[1]=0;p[2]=0}else if('${{ inputs.bump }}'==='minor'){p[1]++;p[2]=0}else{p[2]++};return p.join('.'); } }; + const bump = '${{ inputs.bump }}'; + const pkgs = ['packages/schemas/package.json','packages/core/package.json','packages/react/package.json','packages/cli/package.json']; + let newV; + pkgs.forEach(f => { + const p = JSON.parse(fs.readFileSync(f,'utf8')); + const parts = p.version.split('.').map(Number); + if(bump==='major'){parts[0]++;parts[1]=0;parts[2]=0} + else if(bump==='minor'){parts[1]++;parts[2]=0} + else{parts[2]++} + newV = parts.join('.'); + p.version = newV; + fs.writeFileSync(f, JSON.stringify(p,null,2)+'\n'); + console.log(f, newV); + }); + " + NEW=$(node -p "require('./packages/core/package.json').version") + echo "old=$OLD" >> $GITHUB_OUTPUT + echo "new=$NEW" >> $GITHUB_OUTPUT + + - name: Push branch and open PR + env: + GH_TOKEN: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }} + run: | + BRANCH="chore/bump-v${{ steps.bump.outputs.new }}" + git push origin --delete "$BRANCH" 2>/dev/null || true + git checkout -b "$BRANCH" + git add packages/schemas/package.json packages/core/package.json packages/react/package.json packages/cli/package.json + git commit -m "Bump version to ${{ steps.bump.outputs.new }}" + git push origin "$BRANCH" + gh pr create \ + --title "Bump version to ${{ steps.bump.outputs.new }}" \ + --body "Automated version bump from \`${{ steps.bump.outputs.old }}\` to \`${{ steps.bump.outputs.new }}\`. Merging this PR will trigger tag creation and npm publish of all packages." \ + --base main \ + --head "$BRANCH" diff --git a/.github/workflows/tag-on-merge.yml b/.github/workflows/tag-on-merge.yml new file mode 100644 index 0000000..c48070b --- /dev/null +++ b/.github/workflows/tag-on-merge.yml @@ -0,0 +1,28 @@ +name: Tag and Release on Version Bump + +# Step 2 of 3: fires when a bump PR (chore/bump-vX.Y.Z) merges into main. +# Creates the git tag and GitHub Release, which triggers publish.yml. + +on: + push: + branches: [main] + +jobs: + tag-and-release: + if: contains(github.event.head_commit.message, 'chore/bump-v') + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Create tag and GitHub Release + env: + GH_TOKEN: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }} + run: | + VERSION=$(node -p "require('./packages/core/package.json').version") + git tag "v$VERSION" + git push origin "v$VERSION" + gh release create "v$VERSION" \ + --title "v$VERSION" \ + --generate-notes