Skip to content
Merged
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
8 changes: 0 additions & 8 deletions .claude-plugin/plugin.json

This file was deleted.

36 changes: 2 additions & 34 deletions .github/workflows/create-release-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -260,37 +260,6 @@ jobs:
echo "Updated CHANGES.md for version ${VERSION_NO_V}"
echo "::endgroup::"

- name: Sync Claude plugin version
env:
VERSION_NO_V: ${{ steps.generate_version.outputs.version_no_v }}
run: |
set -euo pipefail
echo "::group::Syncing .claude-plugin/plugin.json"

MANIFEST=.claude-plugin/plugin.json

# Claude caches a plugin under its resolved version, so a skill change only
# reaches users once this version moves. Keeping it equal to the release tag
# makes shipping a skill fix just a matter of cutting a release. The publish
# workflow also reads this file to decide what to tag.
if [[ ! -f "$MANIFEST" ]]; then
echo "::error title=Missing plugin manifest::$MANIFEST not found - Claude plugin users would silently miss this release"
exit 1
fi

OLD_VERSION=$(jq -r '.version // "unset"' "$MANIFEST")
jq --indent 2 --arg v "$VERSION_NO_V" '.version = $v' "$MANIFEST" > "$MANIFEST.tmp"
mv "$MANIFEST.tmp" "$MANIFEST"

NEW_VERSION=$(jq -r '.version' "$MANIFEST")
if [[ "$NEW_VERSION" != "$VERSION_NO_V" ]]; then
echo "::error title=Version sync failed::expected ${VERSION_NO_V}, got ${NEW_VERSION}"
exit 1
fi

echo "::notice title=Plugin Version Synced::.claude-plugin/plugin.json ${OLD_VERSION} -> ${NEW_VERSION}"
echo "::endgroup::"

- name: Open release PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -306,10 +275,10 @@ jobs:
git config --local user.name "GitHub Action"

git checkout -b "$BRANCH"
git add CHANGES.md .claude-plugin/plugin.json
git add CHANGES.md

if git diff --cached --quiet; then
echo "::error title=Nothing to release::CHANGES.md and plugin.json are unchanged - is the Unreleased section empty?"
echo "::error title=Nothing to release::CHANGES.md is unchanged - is the Unreleased section empty?"
exit 1
fi

Expand All @@ -327,7 +296,6 @@ jobs:
"Prepared automatically by the **Create release PR** workflow." \
"" \
"- rolls the \`CHANGES.md\` \`[Unreleased]\` section into \`## ${VERSION_NO_V} - ${RELEASE_DATE}\`" \
"- syncs \`.claude-plugin/plugin.json\` to \`${VERSION_NO_V}\`" \
"" \
"Review the changelog, then merge. Merging triggers **Release publisher**, which tags \`${VERSION}\`, moves \`@v${VERSION_NO_V%%.*}\` and publishes the GitHub release." \
"" \
Expand Down
18 changes: 11 additions & 7 deletions .github/workflows/release-publisher.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
name: Release publisher

# Second half of the release. "Create release PR" prepares a PR; merging it lands the
# rolled CHANGES.md and the synced plugin.json on main, and that merge triggers this.
# rolled CHANGES.md on main, and that merge triggers this.
#
# main is protected by a ruleset and GITHUB_TOKEN cannot be granted a bypass (the bypass
# list takes users, teams and GitHub Apps -- not the built-in Actions token). So nothing
# here pushes to a branch. Tagging is unaffected: the ruleset targets branches, and tags
# are a separate ref namespace.
#
# The version is read from .claude-plugin/plugin.json rather than passed between jobs,
# The version is read from CHANGES.md rather than passed between jobs,
# which makes this idempotent: if that version is already tagged there is nothing to do.
# A re-run or an unrelated push to main is therefore harmless.
#
# There is deliberately no workflow_dispatch. Publishing should be a consequence of
# merging a release PR, never something anyone starts by hand. Combined with the branch
# ruleset, which forbids pushing to main directly, that leaves exactly one way to reach
# this workflow: merge a PR that changes plugin.json. Recovery does not need a manual
# this workflow: merge a PR that changes CHANGES.md. Recovery does not need a manual
# trigger either -- a failed run can be re-run from the Actions UI, and because the
# version comes from the repository rather than from run inputs, the re-run does exactly
# what the original attempt would have done.
Expand All @@ -24,7 +24,7 @@ on:
push:
branches: [main]
paths:
- '.claude-plugin/plugin.json'
- 'CHANGES.md'

concurrency:
group: release-publisher
Expand All @@ -50,9 +50,13 @@ jobs:
set -euo pipefail
echo "::group::Determining version"

VERSION_NO_V=$(jq -r '.version' .claude-plugin/plugin.json)
if [[ -z "$VERSION_NO_V" || "$VERSION_NO_V" == "null" ]]; then
echo "::error title=No version::.claude-plugin/plugin.json has no version"
# CHANGES.md is the source of truth. The first heading carrying a
# semver is the newest released section; `## [Unreleased]` has no
# digits and is skipped, so an unreleased-only changelog matches
# nothing and this fails rather than tagging something arbitrary.
VERSION_NO_V=$(grep -m1 -oP '^##\s+\Kv?[0-9]+\.[0-9]+\.[0-9]+' CHANGES.md || true)
if [[ -z "$VERSION_NO_V" ]]; then
echo "::error title=No version::CHANGES.md has no released version section"
exit 1
fi
VERSION="v${VERSION_NO_V}"
Expand Down
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
!/.github
!/.mkp-builder.ini.example
!/.clinerules
!/skills
!/.claude-plugin
*~
*.swp
*.bak
Expand Down
11 changes: 11 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- The Checkmk plugin development skill has moved to its own repository,
`oposs/cmk-oposs-plugin`. This repository is now only the packaging action. Holding both
behind one version number misfired in both directions: v2.2.2 changed five action files
and no skill files while telling every skill user their skill had updated, and v2.3.0
changed only skill files while cutting an action release whose code was byte-identical.
- The release version is read from `CHANGES.md` instead of `.claude-plugin/plugin.json`,
which went with the skill. A GitHub Action is not a Claude plugin and has no reason to
carry a plugin manifest.

### New

### Changed
Expand Down
42 changes: 14 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,39 +18,25 @@ A reusable GitHub Action for building Checkmk MKP (Monitoring Konfiguration Pack
- 🧹 **Clean**: No permanent changes to your repository
- ⚡ **Fast**: Downloads build tools on-demand, no bloated containers

## Claude Code Plugin: Checkmk Plugin Development Guide
## Related projects

This repo includes a comprehensive Checkmk 2.3.x plugin development guide
as a [Claude Code](https://claude.com/claude-code) skill. Once installed,
Claude will automatically use the guide whenever you ask it to build, upgrade,
or package Checkmk plugins.
This repository is **only** the packaging action. Two companions do the rest:

### Install
- **[`oposs/cmk-oposs-plugin`](https://github.com/oposs/cmk-oposs-plugin)** — the Claude Code
skill for building, upgrading and packaging Checkmk 2.3.x plugins. It used to live here,
which meant a skill change cut a release of this action and an action change told skill
users their skill had updated. Install it with:

Add the OPOSS marketplace and install the plugin:
```
/plugin marketplace add oposs/claude-plugins
/plugin install cmk-oposs-plugin
```

```
/plugin marketplace add oposs/claude-plugins
/plugin install cmk-oposs-plugin@oposs-plugins
```

### What it covers

- Agent-based check plugins, SNMP plugins, special agents
- Metrics, graphing, and perfometers
- Rulesets and GUI configuration
- Bakery integration for automatic agent deployment
- Notification plugins (Slack, Teams, webhooks, etc.)
- Metric migration (renaming metrics while preserving RRD history)
- MKP packaging with the `oposs/mkp-builder` GitHub Action

### Example prompts
- **[`oposs/repo-infra`](https://github.com/oposs/repo-infra)** — release flow, changelog
handling, branch protection and CI. It detects a Checkmk plugin from `.mkp-builder.ini`
and generates a `ci.yml` that calls this action.

- *"Build an SNMP plugin to monitor my Liebert UPS"*
- *"Upgrade my old v1 check plugin to the v2 API"*
- *"Create a notification plugin that sends alerts to Teams"*
- *"Add graphs and configurable thresholds to my plugin"*
- *"Package my plugin as an MKP with a GitHub Actions release workflow"*
The seam between the three is one line: `uses: oposs/mkp-builder@v2`.

## Quick Start

Expand Down
74 changes: 27 additions & 47 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,44 @@
# Releasing

This repo ships two things, released **together** from a single version stream:
This repo ships one thing: the **`mkp-builder` GitHub Action**. Consumers pin
`oposs/mkp-builder@v2`.

- **The `mkp-builder` GitHub Action** — consumers pin `oposs/mkp-builder@v2`.
- **The `cmk-oposs-plugin` Claude Code plugin** — the `skills/` tree, versioned by
`.claude-plugin/plugin.json`.
(The `cmk-oposs-plugin` Claude Code skill used to be released from here too. It now lives
in [`oposs/cmk-oposs-plugin`](https://github.com/oposs/cmk-oposs-plugin) with its own
version stream, because one number could not honestly describe both.)

## The version stream

Git tags (`v2.2.0`, …) with a moving major tag (`@v2`) drive both. A release runs in two
halves, because `main` is protected — see [Why a PR](#why-a-pr) below.
Git tags (`v2.2.0`, …) with a moving major tag (`@v2`). A release runs in two halves,
because `main` is protected — see [Why a PR](#why-a-pr) below.

**1. Run the `Create release PR` workflow** (`workflow_dispatch`, bugfix/feature/major). It:

1. computes the next version from the latest tag, and refuses if that tag already exists,
2. rolls the `CHANGES.md` `## [Unreleased]` section into a dated version section,
3. **rewrites `.claude-plugin/plugin.json` to the same version**,
4. pushes a `release/vX.Y.Z` branch and opens a PR.
3. pushes a `release/vX.Y.Z` branch and opens a PR.

Nothing is tagged or published yet. Closing the PR cancels the release.

**2. Review the changelog and merge the PR.** That triggers `Release publisher`, which
reads the version back out of `plugin.json`, tags it, moves `@vX`, and publishes the
GitHub release with the notes from `CHANGES.md`.
reads the version back out of `CHANGES.md`, tags it, moves `@vX`, and publishes the GitHub
release with the notes from that section.

`CHANGES.md` is the single source of truth for the version. The first `##` heading carrying
a semver is the newest release; `## [Unreleased]` has no digits and is skipped, so a
changelog with nothing released matches nothing and the publisher fails rather than tagging
something arbitrary.

`Release publisher` has **no manual trigger, by design** — publishing should be a
consequence of merging a release PR, never something anyone starts by hand. With `main`
protected against direct pushes, that leaves exactly one route to a release: merge a PR
that changes `plugin.json`. If a run fails, re-run it from the Actions UI; the version
comes from the repository rather than from run inputs, so a re-run does exactly what the
original attempt would have.
protected against direct pushes, that leaves exactly one route to a release. If a run
fails, re-run it from the Actions UI; the version comes from the repository rather than
from run inputs, so a re-run does exactly what the original attempt would have.

Publishing is also idempotent: if the version in `plugin.json` is already tagged it does
nothing, so re-runs and unrelated pushes to `main` are harmless.
Publishing is idempotent: if the newest version in `CHANGES.md` is already tagged it does
nothing. That matters more now than it used to, because the publisher triggers on any push
to `main` that touches `CHANGES.md` — including an ordinary PR that only adds an
`[Unreleased]` entry. Such a push resolves to the already-tagged newest release and stops.

### Why a PR

Expand All @@ -44,36 +50,10 @@ release through a PR instead needs no credential and works with the protection r
than around it. Tagging is unaffected: the ruleset targets branches, and tags live in a
separate ref namespace.

`plugin.json` remains the single source of truth for the plugin version — Claude Code
resolves `plugin.json → marketplace entry → commit SHA`, and `plugin.json` wins. The
marketplace entry in [`oposs/claude-plugins`](https://github.com/oposs/claude-plugins)
therefore carries **no** `version` field for this plugin (a stale duplicate silently
masks the real one). The workflow keeps `plugin.json` equal to the tag so the two
version lines cannot drift apart.

### Releasing a plugin or skill change
### Releasing an action change

1. Make the change under `skills/` and merge it to `main`.
1. Make the change to `action.yml` or `mkp-builder.py`, add a `CHANGES.md` entry under
`## [Unreleased]`, and merge to `main`.
2. Run the **Create release PR** workflow, then merge the release PR it opens.
`plugin.json` is bumped for you — there is no hand-written bump commit any more.
3. Users run `/plugin marketplace update` then `/plugin update cmk-oposs-plugin`.

Nothing else is needed here. Claude only re-resolves plugin versions when it re-fetches
the marketplace, so the marketplace repository has to move as well — but that is handled
from the other side: [`oposs/claude-plugins`](https://github.com/oposs/claude-plugins)
runs an hourly **Track plugin versions** workflow that reads this repository's
`plugin.json` and commits when the version changes.

That direction was chosen deliberately. A push from here would need a credential for
another repository (the org restricts fine-grained PATs, leaving a deploy key or a
GitHub App). Polling from the marketplace needs no credential at all, covers every
plugin instead of only those wired up to push, and still catches a version bumped
outside the release workflow.

> **Why the version bump matters.** Claude caches a plugin under its resolved version and
> reads the skill from `~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/`.
> Merging a skill fix without moving the version ships nothing — every Claude keeps
> reading the old cached copy, with no error and no signal that anything is stale. While
> this was a manual step it was missed often enough that an installed copy was found 10
> commits and three merged skill fixes behind `main`. That is why step 3 of the release
> workflow now enforces it.

Consumers pinning `@v2` pick it up as soon as the major tag moves. Nothing else is needed.
Loading
Loading