diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index d2912ae..2705aca 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -8,7 +8,7 @@
"name": "indigo",
"source": "./",
"description": "Indigo home automation development toolkit \u2014 plugin development, API integration, and control page building",
- "version": "1.9.1",
+ "version": "1.9.2",
"repository": "https://github.com/simons-plugins/indigo-claude-plugin",
"license": "MIT",
"keywords": [
diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index 3a10393..cc07d82 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "indigo",
- "version": "1.9.1",
+ "version": "1.9.2",
"description": "Indigo home automation development toolkit \u2014 plugin development, API integration, and control page building",
"repository": "https://github.com/simons-plugins/indigo-claude-plugin"
}
diff --git a/docs/plugin-dev/workflows/README.md b/docs/plugin-dev/workflows/README.md
new file mode 100644
index 0000000..db269d6
--- /dev/null
+++ b/docs/plugin-dev/workflows/README.md
@@ -0,0 +1,30 @@
+# Canonical CI workflows for Indigo plugins
+
+Drop-in GitHub Actions workflows for any Indigo plugin in the workspace. They auto-detect the plugin bundle (`*.indigoPlugin`) so the same files work in every repo without edits.
+
+## Files
+
+- [`version-check.yml`](version-check.yml) — runs on PR. Extracts `PluginVersion` from `Info.plist` and fails the check if the resulting tag (`v$VERSION` or bare `$VERSION`) already exists. Enforces the workspace rule that every PR bumps the version.
+- [`create-release.yml`](create-release.yml) — runs on push to `main`/`master`. If a tag for the current `PluginVersion` doesn't yet exist, it zips the plugin bundle (excluding `.pyc`, `__pycache__`, IDE files) and creates a GitHub release tagged `v$VERSION` with auto-generated release notes and the zip attached.
+
+## Conventions
+
+- **Tag prefix**: `v$VERSION` (e.g. `v2026.0.3`). The version-check is conservative and rejects either prefixed or bare tags clashing with the new version.
+- **Plugin bundle detection**: `find . -maxdepth 1 -iname "*.indigoPlugin" -type d`. The bundle must live at the repo root. Case-insensitive (`-iname`) so legacy bundles like `HeatmiserNeo.IndigoPlugin` are matched. The detected bundle name is used verbatim throughout (no suffix-stripping with `basename`), so the asset zip preserves the exact directory case (`HeatmiserNeo.IndigoPlugin.zip`, `Domio.indigoPlugin.zip`).
+- **Action versions**: `actions/checkout@v4`, `softprops/action-gh-release@v2`. Both run on Node 20.
+
+## Usage
+
+Copy both YAML files to `.github/workflows/` in the target repo. No edits required — the workflows discover the plugin name at runtime.
+
+```bash
+mkdir -p .github/workflows
+cp /path/to/indigo-claude-plugin/docs/plugin-dev/workflows/version-check.yml .github/workflows/
+cp /path/to/indigo-claude-plugin/docs/plugin-dev/workflows/create-release.yml .github/workflows/
+```
+
+Bump `PluginVersion` in `*.indigoPlugin/Contents/Info.plist`, open a PR. CI will gate the merge on a fresh version; merge to `main` triggers the release.
+
+## Coexistence with other CI
+
+These workflows do not interact with linting/test workflows. Repos with `tests.yml`, `test.yml`, `lint.yml`, etc. can keep them — they run as independent jobs.
diff --git a/docs/plugin-dev/workflows/create-release.yml b/docs/plugin-dev/workflows/create-release.yml
new file mode 100644
index 0000000..0633303
--- /dev/null
+++ b/docs/plugin-dev/workflows/create-release.yml
@@ -0,0 +1,81 @@
+name: Create Release
+
+on:
+ push:
+ branches:
+ - main
+ - master
+
+jobs:
+ create-release:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Determine plugin bundle
+ id: get_plugin
+ run: |
+ PLUGIN_BUNDLE_PATH=$(find . -maxdepth 1 -iname "*.indigoPlugin" -type d | head -1)
+ if [ -z "$PLUGIN_BUNDLE_PATH" ]; then
+ echo "Error: No .indigoPlugin directory found in repository root." >&2
+ exit 1
+ fi
+ PLUGIN_BUNDLE=$(basename "$PLUGIN_BUNDLE_PATH")
+ echo "plugin_bundle=$PLUGIN_BUNDLE" >> $GITHUB_OUTPUT
+ echo "Detected plugin bundle: $PLUGIN_BUNDLE"
+
+ - name: Extract version from Info.plist
+ id: get_version
+ run: |
+ PLUGIN_BUNDLE="${{ steps.get_plugin.outputs.plugin_bundle }}"
+ VERSION=$(grep -A1 'PluginVersion' "${PLUGIN_BUNDLE}/Contents/Info.plist" | grep '' | sed 's/.*\(.*\)<\/string>.*/\1/')
+ if [ -z "$VERSION" ]; then
+ echo "Error: Could not extract PluginVersion from Info.plist." >&2
+ exit 1
+ fi
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ echo "Extracted version: $VERSION"
+
+ - name: Check if release already exists
+ id: check_release
+ run: |
+ VERSION="${{ steps.get_version.outputs.version }}"
+ if git rev-parse "v$VERSION" >/dev/null 2>&1; then
+ echo "exists=true" >> $GITHUB_OUTPUT
+ echo "Release v$VERSION already exists, skipping."
+ else
+ echo "exists=false" >> $GITHUB_OUTPUT
+ echo "Release v$VERSION does not exist, will create."
+ fi
+
+ - name: Create plugin bundle zip
+ if: steps.check_release.outputs.exists == 'false'
+ run: |
+ PLUGIN_BUNDLE="${{ steps.get_plugin.outputs.plugin_bundle }}"
+ zip -r "${PLUGIN_BUNDLE}.zip" "${PLUGIN_BUNDLE}" \
+ -x "*.pyc" \
+ -x "*/__pycache__" \
+ -x "*/__pycache__/*" \
+ -x "*.sublime-project" \
+ -x "*.sublime-workspace" \
+ -x "*/.idea" \
+ -x "*/.idea/*" \
+ -x "*.bbproject" \
+ -x "*.bbproject/*"
+ echo "Created ${PLUGIN_BUNDLE}.zip"
+
+ - name: Create GitHub Release
+ if: steps.check_release.outputs.exists == 'false'
+ uses: softprops/action-gh-release@v2
+ with:
+ tag_name: v${{ steps.get_version.outputs.version }}
+ name: Release v${{ steps.get_version.outputs.version }}
+ generate_release_notes: true
+ draft: false
+ prerelease: false
+ files: ${{ steps.get_plugin.outputs.plugin_bundle }}.zip
diff --git a/docs/plugin-dev/workflows/version-check.yml b/docs/plugin-dev/workflows/version-check.yml
new file mode 100644
index 0000000..7e8d7cd
--- /dev/null
+++ b/docs/plugin-dev/workflows/version-check.yml
@@ -0,0 +1,53 @@
+name: Version Check
+
+on:
+ pull_request:
+ branches:
+ - main
+ - master
+
+jobs:
+ check-version:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Determine plugin bundle
+ id: get_plugin
+ run: |
+ PLUGIN_BUNDLE_PATH=$(find . -maxdepth 1 -iname "*.indigoPlugin" -type d | head -1)
+ if [ -z "$PLUGIN_BUNDLE_PATH" ]; then
+ echo "Error: No .indigoPlugin directory found in repository root." >&2
+ exit 1
+ fi
+ PLUGIN_BUNDLE=$(basename "$PLUGIN_BUNDLE_PATH")
+ echo "plugin_bundle=$PLUGIN_BUNDLE" >> $GITHUB_OUTPUT
+ echo "Detected plugin bundle: $PLUGIN_BUNDLE"
+
+ - name: Extract version from Info.plist
+ id: get_version
+ run: |
+ PLUGIN_BUNDLE="${{ steps.get_plugin.outputs.plugin_bundle }}"
+ VERSION=$(grep -A1 'PluginVersion' "${PLUGIN_BUNDLE}/Contents/Info.plist" | grep '' | sed 's/.*\(.*\)<\/string>.*/\1/')
+ if [ -z "$VERSION" ]; then
+ echo "Error: Could not extract PluginVersion from Info.plist." >&2
+ exit 1
+ fi
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ echo "Extracted version: $VERSION"
+
+ - name: Check if tag already exists
+ run: |
+ VERSION="${{ steps.get_version.outputs.version }}"
+ if git rev-parse "v$VERSION" >/dev/null 2>&1; then
+ echo "::error::Version v$VERSION already exists as a git tag. Please update the PluginVersion in Info.plist."
+ exit 1
+ fi
+ if git rev-parse "$VERSION" >/dev/null 2>&1; then
+ echo "::error::Version $VERSION already exists as a git tag. Please update the PluginVersion in Info.plist."
+ exit 1
+ fi
+ echo "✓ Version $VERSION is new and can be released."