diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..54ce920 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,5 @@ +# Changesets + +This directory contains [**Changesets**](https://github.com/changesets/changesets) which are markdown files that describe package changes for the next release. + +For guidance on when and how to add changesets, checkout the [Maintainer's Guide](../.github/maintainers_guide.md#-updating-changesets). diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..0d310a4 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "restricted", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [], + "privatePackages": { + "version": true, + "tag": true + } +} diff --git a/.github/contributing.md b/.github/contributing.md index 40ae508..3886a77 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -39,6 +39,7 @@ For your contribution to be accepted: - [x] The test suite must be complete and pass. - [x] The changes must be approved by code review. - [x] Commits should be atomic and messages must be descriptive. Related issues should be mentioned by Issue number. +- [x] User-facing changes include a changeset (run `npx changeset add`). See [Updating Changesets](./maintainers_guide.md#-updating-changesets) for the format; releases are automated from these. If the contribution doesn't meet the above criteria, you may fail our automated checks or a maintainer will discuss it with you. You can continue to improve a Pull Request by adding commits to the branch from which the PR was created. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6da89d5..cb11a78 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,12 +6,17 @@ updates: interval: "monthly" open-pull-requests-limit: 5 labels: - - "build" - - "semver:patch" + - "dependencies" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" labels: - - "build" - - "semver:patch" + - "dependencies" + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "monthly" + versioning-strategy: increase + labels: + - "dependencies" diff --git a/.github/maintainers_guide.md b/.github/maintainers_guide.md index 07a635b..7ce7423 100644 --- a/.github/maintainers_guide.md +++ b/.github/maintainers_guide.md @@ -26,7 +26,7 @@ brew update brew install pyenv ``` -Install necessary Python runtimes for development/testing. You can rely on GitHub Actions workflows for testing with various major versions. +Install necessary Python runtime for development/testing. ```sh $ pyenv install 3.14 # select the latest patch version @@ -46,14 +46,50 @@ source .venv/bin/activate ## Versioning -Follow the [conventional commit specification][conv-commits]. PR titles and -commit messages use prefixes like `feat:`, `fix:`, `chore:`, `docs:`, etc. -First letter after the prefix is lowercase unless it's a proper noun. +Follow the [conventional commit specification][conv-commits]. PR titles and commit messages use prefixes like `feat:`, `fix:`, `chore:`, `docs:`, etc. First letter after the prefix is lowercase unless it's a proper noun. -### Plugin version bumps +### 🎁 Updating Changesets -Every release must bump the `version` field in -`.claude-plugin/plugin.json` and `.cursor-plugin/plugin.json` following [semver][semver]. +This project uses [Changesets](https://github.com/changesets/changesets) to track changes and automate releases. + +Each changeset describes a change to the package and its [semver][semver] impact, and a new changeset should be added when updating the package with some change that affects consumers: + +```sh +npx changeset add +``` + +Alternatively, hand-write a file named `.changeset/.md`, with this format: + +```md +--- +"slack": minor +--- + +Add the channel-digest command +``` + +The frontmatter key is always `"slack"`; the value is the [semver][semver] bump level, like `patch`, `minor`, or `major`. The body becomes the changelog entry, so write it for a reader of the release notes. + +Updates to documentation, tests, or CI might not require new entries. + +When a PR containing changesets is merged to `main`, a different PR is opened or updated using [changesets/action](https://github.com/changesets/action) which consumes the pending changesets, bumps the package version, and updates the `CHANGELOG` in preparation to release. + +### 🚀 Releases + +Releasing can feel intimidating at first, but don't fret! Venture on! + +New official package versions are published when the release PR created from changesets is merged. Follow these steps to build confidence: + +1. **Run the tests locally**: Before merging the release PR please run all the tests especially the eval ones. If they no longer pass we may need fix it before releasing the changes. + +2. **Check GitHub**: Please check if issues or pull requests are still open either decide to postpone the release or save those changes for a future update. + +3. **Review the release PR**: Verify that the version bump matches expectations, `CHANGELOG` entries are clear, and CI checks pass. + +4. **Merge and approve**: Merge the release PR. It may take up to 24 hours before you see you release in the [Claude Plugins](https://claude.com/plugins/slack) directory. + +5. **Communicate the release**: + - **External**: Post in relevant channels (e.g. #lang-javascript, #tools-bolt) on [Slack Community](https://community.slack.com/). Include a link to the release notes. ## Everything Else diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9de7060 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,48 @@ +name: Release + +on: + push: + branches: + - main + workflow_dispatch: + +# Don't let two release runs race on the same branch. +concurrency: ${{ github.workflow }}-${{ github.ref }} + +env: + PY_VERSION: "3.14" + NODE_VERSION: "26" + +jobs: + release: + name: Release + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + persist-credentials: false + - name: Set up Node ${{ env.NODE_VERSION }} + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Set up Python ${{ env.PY_VERSION }} + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: ${{ env.PY_VERSION }} + - name: Install Node dependencies + run: npm install + - name: Changesets release + uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0 + with: + version: bash scripts/changeset_version.sh + publish: npx changeset publish + commit: "chore: release" + title: "chore: release" + prDraft: create + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md index eefcb08..4af57f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,3 +58,11 @@ GitHub Actions (`.github/workflows/ci-build.yml`) gates every PR with: - **Test** — `make test-unit` (pytest) LLM-judged tests are not run in CI (Ollama + model download would exceed time budget). + +## Releasing + +Releases are automated and run in CI — **you never run a release yourself.** Your only release-related task is adding a changeset when a PR makes a user-facing change. + +See the [maintainers guide](.github/maintainers_guide.md#-updating-changesets) for the format. + +Everything after that is handled by [changesets](https://github.com/changesets/changesets) and `scripts/changeset_version.sh`: merging to `main` opens a "chore: release" PR, and merging that PR publishes the release. diff --git a/Makefile b/Makefile index 9faff48..a700410 100644 --- a/Makefile +++ b/Makefile @@ -65,7 +65,7 @@ install-tools: $(VENV) ## Install linting/formatting tools (ruff) clean: ## Remove virtual environment, Ollama, and local Cursor install -$(PYTHON) scripts/cursor.py uninstall - rm -rf $(VENV) $(OLLAMA_DIR) + rm -rf $(VENV) $(OLLAMA_DIR) node_modules cursor-install: $(VENV) ## Install this plugin into a local Cursor for development $(PYTHON) scripts/cursor.py install diff --git a/package.json b/package.json new file mode 100644 index 0000000..3748173 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "slack", + "version": "1.1.0", + "private": true, + "engines": { + "node": ">=26" + }, + "devDependencies": { + "@changesets/cli": "^2.31.0" + } +} diff --git a/scripts/changeset_version.sh b/scripts/changeset_version.sh new file mode 100755 index 0000000..6ee35ab --- /dev/null +++ b/scripts/changeset_version.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +npx changeset version +python scripts/sync_versions.py diff --git a/scripts/sync_versions.py b/scripts/sync_versions.py new file mode 100644 index 0000000..12bb3f1 --- /dev/null +++ b/scripts/sync_versions.py @@ -0,0 +1,37 @@ +import json +import logging +from pathlib import Path + +logger = logging.getLogger(Path(__file__).stem) + +REPO_ROOT = Path(__file__).resolve().parent.parent + +# package.json (committed at the repo root) is the version source of truth; changesets +# bumps it, then this script synchronizes the version out to everywhere else its defined. +PACKAGE_JSON_PATH = REPO_ROOT / "package.json" +CLAUDE_PLUGIN_PATH = REPO_ROOT / ".claude-plugin" / "plugin.json" +CURSOR_PLUGIN_PATH = REPO_ROOT / ".cursor-plugin" / "plugin.json" + + +def read_version(package_path: Path) -> str: + """Return the ``version`` field from ``package.json``.""" + return json.loads(package_path.read_text())["version"] + + +def write_version(plugin_path: Path, version: str) -> None: + """Set the ``version`` field of a JSON plugin manifest.""" + manifest = json.loads(plugin_path.read_text()) + manifest["version"] = version + plugin_path.write_text(json.dumps(manifest, indent=2) + "\n") + logger.info(f"Set {plugin_path} version to {version}") + + +def main() -> None: + logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") + version = read_version(PACKAGE_JSON_PATH) + write_version(CLAUDE_PLUGIN_PATH, version) + write_version(CURSOR_PLUGIN_PATH, version) + + +if __name__ == "__main__": + main()