myco:monorepo-release-pipeline
Skill979 starsChanged 19 days agoMITView on GitHub
Covers the full release pipeline for the unifi-mcp monorepo: determining release scope by analyzing changed packages, scoping hatch-vcs version tag globs per Python package to prevent sibling-tag contamination, batching independent release tags while preserving real dependency edges, releasing plugins (independent versions, client update paths, writebacks), configuring scripts/generate_release_notes.py path scoping per package, wiring per-package publish workflows for OIDC trusted publishing, coordinating cross-package version bumps in pyproject.toml, understanding app vs. library versioning and writeback behavior, validating releases post-tag, and verifying shared package architecture constraints (DI-only rule, scope gate, relay sync) before releasing shared packages. The single authoritative procedure for releasing anything in this repo, plugins included. Apply when cutting any release, changing shipped plugin files, bumping unifi-core, or debugging a versioning failure. CRITICAL: All PRs require pin-alignment CI gate before merge (automated, cannot be skipped).
- Deletes or force-pushes
- Installs packages
- Commits and pushes
We split this skill into the rules it gives an agent
42 rules. About 0 are common, 20 are distinctive, and 22 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈20
Distinctive · estimate
This author's own choices. Worth a look.
22
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 35
All feature PRs for the release are merged to main.
- Very specific: line 64
When adding a new package, extend this table, update the release-notes path configuration, and add a new release-<package>.yml workflow …
- Distinctive: line 71
Before pushing any tag, identify exactly which packages changed.
- Distinctive: line 87
After a PR is merged but before tagging, confirm which files actually landed:
- Distinctive: line 118
Decision rule: Default to Minimal. Escalate to Standard when a shared library change requires every dependent to ship a new wheel.
- Distinctive: line 146
When releasing multiple packages in one coordination, avoid bumping major versions for multiple packages simultaneously.
- Distinctive: line 152
When to break the rule: Only when breaking API changes are required in both packages with no feasible bridge, all downstream maintainers …
- Very specific: line 160
Use dynamic = ["version"] with hatch-vcs.
- Distinctive: line 173
Rule: Never expect writeback from a library package tag (core, shared, relay, api).
- Distinctive: line 177
Use patch for bug fixes and backwards-compatible internal changes.
- Very specific: line 211
Every built wheel declares the advisory-safe dependency floors, the Network and API wheels exactly pin aiounifi / aiohttp consistently, and …
- Distinctive: line 222
Transitive chains: Only each package's own declared bounds are validated.
- Very specific: line 228
Before creating release tags, inspect every downstream pyproject.toml for packages being released together.
- Very specific: line 233
Update downstream dependency ranges in apps/*/pyproject.toml and packages/unifi-mcp-relay/pyproject.toml.
- Very specific: line 234
Verify with wheel-metadata check (see below).
- Distinctive: line 235
Commit dependency-bound changes before creating local tags.
- Very specific: line 331
Keep version out of marketplace entries.
- Very specific: line 342
Keep the server pin identical in the inline Claude mcpServers of .claude-plugin/plugin.json and in .mcp.codex.json.
- Very specific: line 344
Never ship a root .mcp.json: OpenClaw would merge it into the server it runs.
- Very specific: line 365
Run make pre-commit (both plugin checks included).
- Very specific: line 387
Add a writeback exclusion between Worker and any tag namespace configured in bump-plugin-versions.yml.
- Distinctive: line 393
Put every tag whose incoming edges are already satisfied into the same batch.
- Very specific: line 432
After a batch containing at least one namespace configured under bump-plugin-versions.yml's on.push.tags:
- Very specific: line 435
Wait for the last bump-plugin-versions.yml run to complete.
- Distinctive: line 443
Start another batch from a writeback commit only when one was created and a later tag actually consumes those committed manifests or a new …
- Distinctive: line 478
All must exit 0 with zero failed/exception records before the first tag in the release is pushed — this is a gate on Procedure F, not just …
- Distinctive: line 482
After pushing a release batch:
- Very specific: line 486
Verify the final manifest sync when triggered: If the batch contains a tag namespace listed in bump-plugin-versions.yml, confirm its last …
- Distinctive: line 505
All three must exit 0 with zero failed/exception records.
- Distinctive: line 507
Post-publish installed-wheel verification: After PyPI confirms the new versions, use the clean environment from step 5 to verify every …
- Very specific: line 532
Strategy B (Proactive): Don't wait for Dependabot PRs to accumulate.
- Distinctive: line 536
pyproject floor vs. lockfile drift: After batching, verify each updated package's lower bound still permits the resolved version.
- Very specific: line 544
Before releasing packages/unifi-mcp-shared/ or packages/unifi-core/, verify these three structural invariants were maintained during …
- Very specific: line 548
Every entrypoint in packages/unifi-mcp-shared/ must accept app-specific behavior as injected parameters.
- Distinctive: line 550
Verify before releasing any shared-package change:
- Very specific: line 564
Import direction must remain strictly left-to-right: app servers → unifi-mcp-shared → unifi-core.
- Distinctive: line 575
Before any shared-package protocol release:
- Very specific: line 586
Sibling tag contamination: If git_describe_command --match is too broad (e.g., v*), hatch-vcs picks up a sibling package's tag and reports …
- Very specific: line 590
Silent version freeze: If a tag is missing, hatch-vcs falls back to fallback_version = "0.0.0".
- Very specific: line 592
Missing tag causes broken downstream install: If unifi-core code is merged but the tag is never pushed, downstream packages requesting that …
- Very specific: line 612
Backwards-coupling enforcement: Import direction must stay: app servers → unifi-mcp-shared → unifi-core.
- Distinctive: line 616
Diagnostics direct-import pattern: When debugging shared-package behavior from an app server, write diagnostic logic in the app server's …
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Monorepo Release Pipeline
The unifi-mcp repo ships seven independently versioned Python packages plus the
Node/TypeScript worker app: unifi-core
and unifi-mcp-shared live under packages/; unifi-network-mcp, unifi-protect-mcp,
unifi-access-mcp, and unifi-api-server live under apps/; unifi-mcp-relay lives under packages/
alongside core and shared; unifi-mcp-worker lives under apps/worker/ and publishes to npm.
Each has its own package identity, tag namespace, publish workflow, and release-notes scope. Getting the release sequence wrong leaves downstream
packages referencing non-existent PyPI versions or produces contaminated release notes
that bleed across package boundaries.
Prerequisites
- All feature PRs for the release are merged to
main. - PRs that change any package's
pyproject.tomlorsrc/must pass the pin-alignment CI gate (automated blocker). The gate builds the workspace wheels and installs them the way a user would, so a downstream floor that cannot work on PyPI fails before release. See Procedure D, Step 0 for exactly what it covers and what it cannot see. - Working tree is clean:
git statusshows nothing staged or modified. - Remote is current:
git fetch origin && git log origin/main..HEADshows nothing. - PyPI credentials are not stored locally — publishing is handled entirely by
GitHub Actions OIDC trusted publishing (no
TWINE_PASSWORD, noPYPI_TOKEN). - Decide which packages are changing and their new versions before pushing any tag.
Package Map
| Package | Directory | PyPI Name | Tag namespace |
unifi-core | packages/unifi-core/ | unifi-core | core/v* |
unifi-mcp-shared | packages/unifi-mcp-shared/ | unifi-mcp-shared | shared/v* |
unifi-mcp-network | apps/network/ | unifi-network-mcp | network/v* |
unifi-mcp-protect | apps/protect/ | unifi-protect-mcp | protect/v* |
unifi-mcp-access | apps/access/ | unifi-access-mcp | access/v* |
unifi-api-server | apps/api/ | unifi-api-server | api/v* |
unifi-mcp-relay | packages/unifi-mcp-relay/ | unifi-mcp-relay | relay/v* |
unifi-mcp-worker | apps/worker/ | unifi-mcp-worker (npm, unscoped) | worker/v* |
Critical: PyPI package names differ from directory names. Always reference the PyPI name when
installing or checking versions. Example: pip install unifi-network-mcp (not unifi-mcp-network).
The worker's npm package name is also unscoped: npm install -g unifi-mcp-worker (not
@unifi-mcp/worker) — confirmed by apps/worker/src/commands/upgrade.mjs's npm view unifi-mcp-worker version call.
When adding a new package, extend this table, update the release-notes path configuration,
and add a new release-<package>.yml workflow before pushing any tag.
Procedure A: Determine Release Scope
Before pushing any tag, identify exactly which packages changed. This decision gates everything downstream — wrong scope means tagging unnecessary packages, causing spurious releases, or forgetting a tag and leaving a gap in PyPI versions.
List changes per package
For each package candidate, check whether code actually changed since the last tag:
# Check packages/unifi-core/:
git log --oneline core/v$(git tag -l 'core/v*' | sort -V | tail -1)..HEAD -- packages/unifi-core/
# Check apps/network/:
git log --oneline network/v$(git tag -l 'network/v*' | sort -V | tail -1)..HEAD -- apps/network/ packages/unifi-mcp-shared/Post-merge scope verification
After a PR is merged but before tagging, confirm which files actually landed:
# Replace <pre-merge-sha> with the commit SHA immediately before the merge commit
git diff <pre-merge-sha>..HEAD --name-onlyThis is the most reliable way to scope which packages need a release — PR descriptions can lag or overstate changes.
Apply scope rules
| What changed | Tags required |
packages/unifi-mcp-shared/ only | Always shared/v*; add affected downstream tags under the selectivity framework, then use Procedure F for batch boundaries |
packages/unifi-core/ only | Scope includes core/v* and affected downstream packages, including api/v*; Procedure F decides batch boundaries |
One app only (e.g., apps/protect/) | protect/v* only |
| Multiple apps | One tag per changed app; put independent tags in the same release batch |
| Plugin-only changes (any shipped file) | Independent plugin manifest bump; no package tag (Procedure E) |
Worker (apps/worker/) | worker/v* |
Negative corollary (skip-unchanged rule): Do NOT tag a package simply because an upstream dependency was bumped, if the package's own code did not change AND its existing pyproject.toml bounds already accommodate the new upstream version. Check the declared version range in pyproject.toml first — if the new upstream version already satisfies the existing bounds and the app code is unchanged, no new tag is required.
Tagging Selectivity Framework
Minimal (lowest risk): Tag ONLY packages with code changes in their own directory.
Standard (recommended): Tag all packages with code changes PLUS all downstream dependents to ensure they pick up shared library bumps. Use for any bump to unifi-core or unifi-mcp-shared.
Full (maximum redundancy): Tag all seven packages every release. Use for scheduled releases or recovering from a prior broken release sequence.
Decision rule: Default to Minimal. Escalate to Standard when a shared library change requires every dependent to ship a new wheel. Use Full when recovering from a release failure.
Plugin releases
Plugins release by merge, not by tag. Follow Procedure E.
Shared package rule
Any change to packages/unifi-mcp-shared/ requires a Shared release. Add a downstream app or Relay
tag when its own code changed, it needs the new Shared API or metadata floor, or the release plan
explicitly selects Standard or Full coverage. Under the default Minimal strategy, unchanged
dependents whose existing bounds accept the new Shared version do not need tags. Procedure F decides
batch boundaries after scope is settled.
CVE / transitive dependency changes
Trace the dependency chain before tagging everything:
grep -ri "pillow" apps/*/pyproject.toml packages/*/pyproject.tomlTag only the affected app(s). Unnecessary tags create spurious releases.
Procedure A.5: Patch-Bump Lockstep Avoidance
When releasing multiple packages in one coordination, avoid bumping major versions for multiple packages simultaneously. This prevents accidental lockstep coupling.
Strategy: When releasing shared with core, give core the larger bump (minor/major as needed) while keeping shared's bump narrow (patch-only, staying within its current minor). Defer shared's next minor bump to a future cycle. Downstream apps can adopt the new core version while keeping the prior shared minor pinned, then adopt the new shared minor independently later.
Verify current floor bounds before citing an example — they shift every release. Re-check the live dependency line with grep unifi-core packages/unifi-mcp-shared/pyproject.toml rather than trusting a hardcoded version string from this skill.
When to break the rule: Only when breaking API changes are required in both packages with no feasible bridge, all downstream maintainers are coordinated, and you can support a temporary dual-version install guide.
Procedure B: App vs. Library Versioning and Writeback Behavior
Library packages (unifi-core, unifi-mcp-shared, relay, api)
Use dynamic = ["version"] with hatch-vcs. Version is derived from the git tag at build time — no _version.py is ever committed. When a library tag is pushed, bump-plugin-versions.yml outputs No version changes to commit — this is correct and expected.
unifi-api-server is a library package, not an app package. It publishes to PyPI only and has no plugin manifest assets. Expect no writeback on api/v* tags.
App packages (network, protect, access)
These have writable manifest assets updated on tag:
plugins/unifi-network/.claude-plugin/plugin.jsonplugins/unifi-protect/.claude-plugin/plugin.json- Corresponding
.codex-plugin/plugin.jsonand.mcp.codex.json plugins/unifi-access/.claude-plugin/plugin.jsonapps/*/server.json(server package version)
Rule: Never expect writeback from a library package tag (core, shared, relay, api). Never accept a missing writeback from an app package tag (network, protect, access).
Version bump selection
Use patch for bug fixes and backwards-compatible internal changes. Use minor when adding
new functionality that is additive but could affect downstream consumers — even if technically
backwards-compatible, a minor bump signals to dependents that review is warranted. Use major
for breaking API changes. Dependency bound updates for upstream packages must land on main
(and pass CI) before pushing the corresponding version tags.
Procedure C: hatch-vcs Tag Glob Scoping
Without a per-package git_describe_command --match glob, any tag reachable in the repo can influence any package's version.
| Package | Match pattern |
| network | network/v* |
| protect | protect/v* |
| access | access/v* |
| relay | relay/v* |
| unifi-core | core/v* |
| unifi-mcp-shared | shared/v* |
| api | api/v* |
Each package must declare both tag_regex and git_describe_command scoped to its own prefix. Never share or widen the --match pattern across packages.
Procedure D: Pin-Alignment CI Gate + Align Cross-Package Dependency Bounds Before Tagging
Step 0: Pin-Alignment CI Gate (Automated Blocker)
The pin-alignment CI gate (scripts/check_pin_alignment.py, workflow check-pin-alignment.yml) runs on a PR that touches any of: a package's pyproject.toml, anything under apps/*/src/** or packages/*/src/** (core, shared, relay and every app), the API's force-included alembic files, the gate script, the workflow, or scripts/generate_api_action_catalog.py. If your PR fails: read the CI output for the failing package and module, update the offending bounds in pyproject.toml (or split the change, see the floor-bump sequencing gotcha), commit and push.
What the gate proves, against PyPI plus the workspace-built upstream wheels:
- Every built wheel declares the advisory-safe dependency floors, the Network and API wheels exactly pin
aiounifi/aiohttpconsistently, and the prior vulnerable resolutions upgrade to safe versions. - Each downstream wheel installs into a clean venv, with the declared ranges resolved from PyPI. Then every module the wheel ships is imported. The module list comes from the installed distribution's file list, so directories without an
__init__.py(Network'stools/andutils/) are included. AnyImportErrorfails the gate: an undeclared dependency (ModuleNotFoundError) and a symbol the resolved upstream does not provide (cannot import name). Other import-time exceptions are ignored. - Each downstream wheel is installed again against exactly its declared minimum
unifi-core(and, for Protect, Access and Relay, its declared minimumunifi-mcp-shared). The same full module import runs against those floors, so a symbol imported from a newer upstream than the floor admits fails even though the latest upstream has it. - An AST scan of the installed downstream source resolves every attribute reached through an
unifi_core/unifi_mcp_sharedimport (import unifi_core.x as x; x.new_helper,from unifi_core.x import Cls; Cls.new_method) against those floor versions. This covers use that only fails when the line runs. - Network and API additionally run manager-call contracts against the Core floor: every direct
<name>_manager.<method>call in the Network tool modules, and every API catalog binding, must exist with a compatible signature.
What the gate cannot see (verify these by hand):
- Calls on objects the scan cannot resolve statically. Attribute access on instances, parameters, return values, or any name that is rebound in the same file (
client = Cls(); client.new_method()) is skipped. Network and API are covered for manager calls by their contracts; Protect, Access and Relay have no equivalent, so a new Core method called on a manager instance without a floor bump passes. Review any PR that starts calling new upstream methods against the upstream's changelog. - Behavior changes with the same API shape. A floor is checked for symbol existence and signatures, not semantics.
- The Shared floor of Network and API. Network is installed with the branch-built Shared candidate (the release being cut), and the API wheel declares no direct Shared dependency, so neither is tested against its minimum published Shared.
- Transitive chains. Only each package's own declared bounds are validated; see the coverage limit below.
- Imports that do not run at import time. An import inside a function body never runs when the module is imported, and an import under
try/except ImportErrorhas its failure swallowed; the attribute scan resolves neither. A symbol absent from the floor that is only imported that way passes. - Import-time exceptions other than
ImportError(for example a module that needs a live controller at import) are ignored, so that module's imports after the failure point go unchecked.
Align Cross-Package Dependency Bounds Before Tagging
Before creating release tags, inspect every downstream pyproject.toml for packages being released together. Tag order only solves publication timing; the wheel metadata must also allow the newly published upstream version.
Dependency-bound checklist:
- Identify upstream packages that changed (
unifi-core,unifi-mcp-shared). - Identify downstream packages being tagged because they use that upstream code.
- Update downstream dependency ranges in
apps/*/pyproject.tomlandpackages/unifi-mcp-relay/pyproject.toml. - Verify with wheel-metadata check (see below).
uv lock --checkis insufficient — see below. - Commit dependency-bound changes before creating local tags.
Why uv lock --check is not a sufficient gate
Workspace [tool.uv.sources] overrides take precedence over the version range during uv sync/uv lock — every CI job passes cleanly even when the published wheel's requires_dist will reject the just-released upstream package. The pin only fails when pip/uv resolves the published wheel against PyPI on a user's machine. Docker images also bypass the failure. This is a PyPI-only failure mode.
check_pin_alignment.py Coverage Limit
scripts/check_pin_alignment.py validates the direct dependency bounds declared in each package's own pyproject.toml — it does not walk the full transitive/nested dependency chain (e.g., relay → shared → core, multiple hops deep). A downstream package can pass the gate while a deeper transitive package in the chain is still misaligned. When releasing a change that touches a multi-hop dependency chain, manually trace and verify bounds at each hop rather than trusting a single green run of this script.
Running one floor check locally: --network-core-floor-only and --api-core-floor-only
The full gate takes several minutes because it builds every wheel and builds a venv per downstream. When a PR changes only how Network or the API uses upstream code, run just that wheel's floor check:
uv run scripts/check_pin_alignment.py --network-core-floor-only
uv run scripts/check_pin_alignment.py --api-core-floor-onlyEach mode builds that one wheel, installs it against exactly its declared unifi-core minimum, and runs the manager-call contract, the full module import, and the upstream attribute scan described in Step 0. A failure means the code was written against an upstream API newer than the floor admits: either raise the floor (after the upstream version is on PyPI) or remove the use. Protect, Access and Relay have no single-package flag; they are checked by the full gate.
Pre-tag wheel-metadata check
rm -rf /tmp/wheelcheck && mkdir -p /tmp/wheelcheck
for app in apps/network apps/protect apps/access packages/unifi-mcp-relay; do
echo "=== $app ==="
uv build --wheel "$app" --out-dir /tmp/wheelcheck/$(basename $app) 2>&1 | tail -2
whl=$(ls /tmp/wheelcheck/$(basename $app)/*.whl | tail -1)
python -m zipfile -e "$whl" /tmp/wheelcheck/extracted/$(basename $app)/
grep -h 'Requires-Dist.*\(unifi-mcp-shared\|unifi-core\)' \
/tmp/wheelcheck/extracted/$(basename $app)/*.dist-info/METADATA
doneProcedure E: Plugin Releases
A plugin release is a plugin version bump landing on main. There is no publish
step: every client reads the default branch unless the user pins a ref. Python
packages and the Worker still release only by tag.
Triggers
- Plugin-only release: a PR that bumps the plugin version merges to
main. - Server release: a Network, Protect or Access tag runs
bump-plugin-versions.yml, which moves the server pin and bumps the plugin in one writeback commit.
Clients
| Client | Reads | Detects an update by | Users update with |
| Claude Code | Marketplace on the default branch unless added as owner/repo#ref | Manifest version; the cache is keyed by it | claude plugin marketplace update unifi-plugins, then claude plugin update <plugin>@unifi-plugins (or /plugin → Installed → Update now), then a new session: the SessionStart notice does not run on /reload-plugins. Auto-update is off by default for our marketplace; a user can turn it on under /plugin → Marketplaces, and it then installs releases in the background during a session and prompts for /reload-plugins, which also skips the notice. |
| Codex | .agents/plugins/marketplace.json on remote HEAD unless added as owner/repo@ref | Manifest version: Codex re-clones configured Git marketplaces at the start of every session (and on codex plugin marketplace upgrade) and reinstalls a plugin only on a version change | Nothing: Codex runs the bundled server, which reads the per-user settings file setup writes (UNIFI_SETTINGS_FILE), so a pin bump starts in the session after the one that installs the release. A [mcp_servers.<plugin>] entry from setup before plugin 1.0.0 shadows the bundled server until the user runs setup with --refresh once |
| OpenClaw | git:owner/repo[@ref] or our Claude-format marketplace; treats the bundle as a Codex bundle and runs the server in .mcp.codex.json, merging in a root .mcp.json if one ships (so none does) | Commits, not version | openclaw plugins update <id> or openclaw plugins update --all; it asks for capability consent, or needs --accept-capabilities without a terminal. The bundled server reads the per-user settings file setup writes, so the pin moves at the next agent turn, without a Gateway restart. An mcp.servers.<plugin> entry from setup before plugin 1.0.0 replaces the bundled server until the user runs setup with --target openclaw --refresh once |
| Hermes Agent | Cannot load the current layout (needs a root plugin.json, skills/ and mcp.json) | Commits | Not supported |
Sources: Claude Code host a marketplace,
loading,
install,
marketplace reference;
Codex plugins and openai/codex
core-plugins/src/marketplace_upgrade.rs, loader.rs;
OpenClaw install,
update,
bundles;
Hermes Agent plugins.md and plugin-catalog.md (NousResearch/hermes-agent).
Rules
- One release, one merge. Land every plugin change that makes up one release on
maintogether, never as a string of separate merges. Commit-tracking clients (OpenClaw, Hermes) deliver every commit to their users. - Never ahead of the server. Land plugin content (skills, references, setup, MCP config) that depends on a server feature only when, or after, that server version is tagged and the pin moves; usually in or after the writeback.
- Enforcement.
make check-plugin-versionsadmits version bumps and pins;make check-plugin-tool-refsfails when a shipped plugin file names a tool absent from the release tag its product's plugin pins (unifi-intelligence: the product pins plus the latest released cloud worker). Both run inmake pre-commitand in the generated-artifacts CI job. Never weaken either check to pass a PR; fix the plugin text or move the pin. - Counts and references follow the pin. Plugin skill tool counts and the generated
references/*-tools.mdcome from the manifest of the release tag each plugin pins (git show <tag>:<manifest>), never the workspace: a server PR that adds a tool leaves them unchanged, and the plugin release that moves the pin (the writeback runsgenerate_skill_references.py) updates them. A new tool category needs its<!-- AUTO:tools:<category> -->marker section added in that release.
Versioning
- Each plugin has an independent SemVer describing everything it ships, separate from
the server pin. Set the same
versionin every existing Claude and Codex manifest (at least one). The skills-onlyunifi-intelligencebundle has both manifests, no MCP config and no pin, and names the MCP plugins it needs in the Claude manifest'sdependencies; Codex and OpenClaw install no dependencies, so its descriptions say so. Keepversionout of marketplace entries. - A renamed plugin is a new identity to every client, so it starts at
1.0.0. The guard checks a plugin with no files at the merge base as new and skips a removed one; say in the upgrade guide what users of the old name do. - Bump every affected plugin in the same PR whenever any file under
plugins/<name>/changes, including skills, references, setup scripts, MCP configs and docs. Version-only bumps are allowed. - Patch: plugin-only fixes or docs, and patch server pin moves. Minor: new skills/capabilities, and minor server pin moves. Major: breaking setup/configuration, and major server pin moves. Before 1.0, breaking changes advance minor and additive changes advance patch (SemVer 0.x).
- Keep the server pin identical in the inline Claude
mcpServersof.claude-plugin/plugin.jsonand in.mcp.codex.json; it must name a release tag. Never ship a root.mcp.json: OpenClaw would merge it into the server it runs. - Writeback:
bump-plugin-versions.ymlrunspython3 scripts/plugin_versions.py sync --releasing <package-tag>. It moves the inline Claude pin and the.mcp.codex.jsonpin, bumps the plugin by the size of the server move, regeneratesapps/<product>/server.jsonwith the server version, and inserts oneCHANGELOG.mdentry naming every plugin it bumped and linking each server release. Retrying an already-synced pin does not bump again. Core, Shared and Relay triggers only coalesce outstanding product tags, otherwise they write nothing. make check-plugin-versionsrunsscripts/plugin_versions.py check --base origin/main; PR CI uses the PR base SHA. Fetch base history and tags first; a missing base fails. Changed plugin versions must be valid SemVer, agree across manifests and exceed the merge-base version, andCHANGELOG.mdneeds an entry heading naming<plugin> <version>for each changed or new plugin. Pins must agree and name release tags; only the workflow's exact--releasingtag may stand in for a tag not yet discovered locally.
Release checklist
- Bump the version by the rules above and add a
CHANGELOG.mdentry below its marker:## <YYYY-MM-DD UTC> — <plugin> <version>[, <plugin> <version>…], then the user-visible changes in plain language (what users notice, not how it was built). Repeat them in the PR description. The README describes what each plugin is; never put release notes there. - Run
make pre-commit(both plugin checks included). - Merge as one squash merge.
- Tell users how to update, per the client table: Claude Code users update the
marketplace and plugin themselves (unless they turned on auto-update), then start a
new session; Codex installs the release and its pinned server at the next session
start (users whose
config.tomlentry predates plugin 1.0.0 run setup with--refreshonce); OpenClaw users runopenclaw plugins update. - From
main, after the merge, run the real-client acceptance procedure indocs/plugin-acceptance.md. - Roll back by reverting the merge, then bumping the version again above the bad release; the version guard requires the higher version and clients keyed by version need the change to reinstall.
Procedure F: Dependency-Aware Release Batching
Build a dependency and writeback graph before pushing tags. A package has a hard dependency edge on an upstream release when its wheel metadata must name the new upstream version, its code requires an API that exists only in that version, or its release workflow installs the new version from PyPI. Existing bounds that already accept the upstream release do not create an edge by themselves.
Add a writeback exclusion between Worker and any tag namespace configured in
bump-plugin-versions.yml. The Worker release and plugin-version sync both checkout and push
main, but they do not share a concurrency group or retry a non-fast-forward push. Keep Worker out
of those batches. Wait for the plugin writeback to finish, fetch main, and then push Worker; an
API-only tag may share the Worker batch because it does not start the plugin writeback.
Put every tag whose incoming edges are already satisfied into the same batch. Create the tags from
the intended release commit, then push each tag in its own command back-to-back. Separate push
commands reliably trigger every tag workflow; one multi-ref git push can omit triggers.
# Include only packages in this batch. Each variable is the reviewed version without the v prefix.
release_tags=(
"core/v${CORE_VERSION:?Set CORE_VERSION}"
"shared/v${SHARED_VERSION:?Set SHARED_VERSION}"
"network/v${NETWORK_VERSION:?Set NETWORK_VERSION}"
"protect/v${PROTECT_VERSION:?Set PROTECT_VERSION}"
"access/v${ACCESS_VERSION:?Set ACCESS_VERSION}"
"api/v${API_VERSION:?Set API_VERSION}"
"relay/v${RELAY_VERSION:?Set RELAY_VERSION}"
"worker/v${WORKER_VERSION:?Set WORKER_VERSION}"
)
# Each loop iteration is a separate push. Do not wait for its workflow before the next iteration.
for tag in "${release_tags[@]}"; do
git tag "$tag" || exit 1
done
for tag in "${release_tags[@]}"; do
git push origin "$tag" || exit 1
doneMonitor every workflow in the batch concurrently. Wait at a dependency boundary only: confirm the upstream workflow and PyPI artifact before pushing a later batch that consumes that artifact. If one workflow fails, let unrelated workflows finish, but hold every downstream batch that depends on the failure.
Consolidated plugin-manifest writebacks
bump-plugin-versions.yml reads the current release tags and synchronizes all Network, Protect, and
Access server pins and independently bumps each changed plugin. Its concurrency policy coalesces a burst of tags: earlier sync runs may be
cancelled, and the last run writes one commit containing every app version visible at that point.
Cancelled superseded runs are expected; the final run must succeed.
After a batch containing at least one namespace configured under
bump-plugin-versions.yml's on.push.tags:
- Wait for the last
bump-plugin-versions.ymlrun to complete. - For a batch with Network, Protect, or Access tags, fetch
origin/main, inspect the consolidated writeback commit, and verifyapps/<app>/server.jsonand both MCP configs contain each released server version; existing plugin manifests must agree on the independently bumped plugin version. Runmake check-plugin-versionsagainst the parent of the writeback commit. - For a library-only trigger batch containing Core, Shared, or Relay, require the sync run to
succeed but allow
No version changes to commit. Inspect a commit only if the run found an outstanding app-version change. - Start another batch from a writeback commit only when one was created and a later tag actually consumes those committed manifests or a new dependency pin. Independent tags can share the original release commit.
For example, direct security floors added independently to Shared, Network, Protect, Access, API, and Relay form one batch when no cross-package floor changes. A new Core API plus downstream code that requires it forms at least two batches: publish Core first, then publish the downstream wheels after Core is available and their reviewed metadata floors are correct.
Floor-bump sequencing gotcha: Open downstream
pyproject.tomlfloor-bump PRs (raising the minimum version bound on an upstream package) only after the upstream tag is confirmed on PyPI. Committing the floor-bump PR before the upstream version exists on PyPI causes the pin-alignment CI gate on that PR to fail — the gate tries to resolve the declared lower bound but the version does not yet exist.
Floor-bump local staging: While waiting for PyPI to confirm the upstream version, stage the floor-bump work locally — create the branch, update downstream
pyproject.tomlversion floors, and commit locally. Holdgit pushand PR creation until PyPI confirms the version exists. This decouples preparation from the PyPI propagation gate and eliminates idle waiting between confirmation and branch push.
Post-hoc branch-split at package boundary: If a shared/core change was developed inline inside a larger feature branch that also touches a downstream package (e.g.,
unifi-api-server), split the reviewed core change into its own clean branch and PR before merging the downstream work. Verify the core PR independently, squash-merge and tag it first, then continue the downstream package's PR on top of the merged core change. This preserves the dependency-ordered tag sequence above even when development didn't originally follow package boundaries.
Procedure G: generate_release_notes.py Path Configuration
Open scripts/generate_release_notes.py and locate APP_CONFIGS. Each entry is a PackageConfig with path_groups — a tuple of PathGroup objects that filter PRs to only those touching paths relevant to each package. Each entry should include the app directory, shared dependency directories (packages/unifi-core/, packages/unifi-mcp-shared/), and its own publish, test, and Docker build workflow paths.
Known limitation: scripts/generate_release_notes.py does not emit PR author information. Contributor credit must be added manually.
Procedure G.5: Pre-Tag Full Release-Wide Smoke Matrix Gate
Even when the merged diff touches only one package (e.g., a Core-only PR), do not tag from that PR's own targeted live validation alone. Run the full release-wide safe-phase smoke matrix across every live-validated surface (network, protect, access, api) before pushing any tag. A single package's isolated pass does not prove the other surfaces still function against the same rebased head — regressions in unrelated surfaces have shipped from Core-only changes that looked locally safe.
uv run python scripts/live_smoke.py --server network --phase safe
uv run python scripts/live_smoke.py --server protect --phase safe
uv run python scripts/live_smoke.py --server access --phase safeAll must exit 0 with zero failed/exception records before the first tag in the release is pushed — this is a gate on Procedure F, not just a post-tag check.
Procedure H: Release Validation
After pushing a release batch:
- Check CI concurrently: Capture every release workflow run and confirm all complete successfully. Do not serialize independent workflow watches.
- Verify the final manifest sync when triggered: If the batch contains a tag namespace listed
in
bump-plugin-versions.yml, confirm its last run succeeded. For Network, Protect, or Access tags, inspect the consolidated commit onmain; for a Core-, Shared-, or Relay-only trigger, accept a successfulNo version changes to commitresult. API-only and Worker-only batches skip this step because they do not trigger the workflow. - Verify local tag versions: For each Python tag, change to its directory from the Package Map
and run
hatch version; it must print exactly the tagged version. Worker is not a Hatch project, so verify its version from the exact npm artifact in the next step. - Confirm every registry artifact: Query PyPI for each exact Python version and run
npm view unifi-mcp-worker@<version> versionfor a Worker tag. - Install smoke test: Install compatible releases together in one clean environment when
possible. Assert each installed distribution version, its import path under
site-packages, and any security or cross-package metadata floor that motivated the release. - Post-Release Live Smoke Verification (must run via
uv, not systempython3):
All three must exit 0 with zero failed/exception records. This is the final release validation gate. Do not invoke with bareuv run python scripts/live_smoke.py --server network --phase safe uv run python scripts/live_smoke.py --server protect --phase safe uv run python scripts/live_smoke.py --server access --phase safepython3 scripts/live_smoke.py— the system Python lacks the workspace dependencies and the harness will fail at import time. - Post-publish installed-wheel verification: After PyPI confirms the new versions, use the
clean environment from step 5 to verify every released Python distribution. For released server
apps, run
live_smoke.py --server <server> --phase safeseparately with that environment's Python executable, notuv runor--server all. Core, Shared, API, and Relay have no standalone live-smoke target, so their installed version andsite-packagesimport-path assertions from step 5 are their packaging gate. Worker uses the exact npm-artifact check from step 4. This catches packaging defects that local workspace sources can hide.
Procedure I: Dependabot Dependency Update Management
Dependabot updates the lockfile one package at a time. When multiple PRs fail CI with the same class of lockfile error, merge into a single maintainer branch:
git checkout -b deps/batch-dependabot main
# Apply dependency changes, then re-lock from combined state:
uv lock
# Run per-package tests before opening the PR:
cd apps/network && uv run pytest && cd ../..
cd apps/protect && uv run pytest && cd ../..Strategy A (Reactive): Batch accumulated Dependabot PRs into a single maintainer branch, run uv lock, merge the combined PR.
Strategy B (Proactive): Don't wait for Dependabot PRs to accumulate. Periodically run uv lock yourself and open a single maintainer PR before the Dependabot queue grows. This prevents cascading CI failures from multiple simultaneous lockfile-touching PRs.
Major-version bumps are invisible at the PR level — the CI failure looks identical to a minor version conflict. Always inspect the actual version change in the PR diff.
pyproject floor vs. lockfile drift: After batching, verify each updated package's lower bound still permits the resolved version. Run the wheel-metadata check from Procedure D before merging.
Second-wave cascading PRs gotcha: After a batch Dependabot PR merges, Dependabot may open a fresh wave of new PRs for versions newly unlocked by the combined resolution. This is expected — the first merge freed resolution space for the next tier. Budget time for a second round of batching review.
Procedure J: Shared Package Architecture Constraints
Before releasing packages/unifi-mcp-shared/ or packages/unifi-core/, verify these three structural invariants were maintained during development.
J-1: DI-Only Rule — No Reverse Imports in unifi-mcp-shared
Every entrypoint in packages/unifi-mcp-shared/ must accept app-specific behavior as injected parameters. Direct imports from unifi_network_mcp, unifi_protect_mcp, or unifi_access_mcp inside the shared package create a circular import that breaks all three app servers simultaneously at import time — blast radius is total.
Verify before releasing any shared-package change:
grep -r "from unifi_network_mcp\|from unifi_protect_mcp\|from unifi_access_mcp" packages/unifi-mcp-shared/Any match is a release blocker. Replace with an injected parameter or a typed Protocol interface defined within packages/unifi-mcp-shared/.
J-2: Scope Gate — unifi-core vs. unifi-mcp-shared
Decision tree for placing new code:
- Talks directly to UniFi hardware or manages HTTP sessions? →
unifi-core - Coordinates MCP concerns (permissions, tool registration, confirmations)? →
unifi-mcp-shared - References MCP types (
FastMCP, tool decorators)? →unifi-mcp-shared - Would be useful in a non-MCP CLI calling the UniFi API? →
unifi-core
Import direction must remain strictly left-to-right: app servers → unifi-mcp-shared → unifi-core. Verify:
grep -r "from unifi_mcp_shared\|import unifi_mcp_shared" packages/unifi-core/
grep -r "from unifi_network_mcp\|from unifi_protect_mcp\|from unifi_access_mcp" packages/unifi-mcp-shared/ packages/unifi-core/Both commands must return nothing.
J-3: Relay Protocol Sync (Required Before Shared-Package Release)
The two relay files packages/unifi-mcp-relay/src/unifi_mcp_relay/discovery.py and packages/unifi-mcp-relay/src/unifi_mcp_relay/protocol.py implement relay protocol logic that does not import from unifi-mcp-shared. When shared-package protocol changes (new message format, endpoint path, header), these files receive no automatic update — there is no import error, failing test, or CI gate. Drift fails silently at runtime.
Before any shared-package protocol release:
- Identify what changed (message schema, endpoint path, header, error envelope shape).
- Open
packages/unifi-mcp-relay/src/unifi_mcp_relay/discovery.pyandpackages/unifi-mcp-relay/src/unifi_mcp_relay/protocol.pyand manually port the change. - Trace the message path end-to-end through both to confirm they agree.
PR checklist trigger: any PR modifying shared-package protocol must include a "relay sync" section confirming both relay files were reviewed.
Cross-Cutting Gotchas
Sibling tag contamination. If git_describe_command --match is too broad (e.g., v*), hatch-vcs picks up a sibling package's tag and reports the wrong version. Fix: tighten the --match pattern and rebuild.
PR merge is NOT the release trigger — the tag push is. hatch-vcs reads git tags at build time. Merging a PR does NOT trigger a release. Always run Procedure H after tagging.
Silent version freeze. If a tag is missing, hatch-vcs falls back to fallback_version = "0.0.0". There is no error at merge time. Always run Procedure H after tagging.
Missing tag causes broken downstream install. If unifi-core code is merged but the tag is never pushed, downstream packages requesting that version fail to install. Check PyPI before debugging code.
Plugin updates need a version change. Claude Code and Codex install a merged plugin change only when its manifest version changes. Keep the bump in the same PR as the shipped-file change (Procedure E).
Malformed git tag — missing 'v' prefix. A tag like protect/0.4.2 (no 'v') is syntactically wrong and ignored by hatch-vcs. Validate before pushing: git tag -l | grep -E 'network|protect|access|relay|core|shared|api' | sort -V — every line must contain /v.
PyPI pin masked by workspace source — failure mode the entire CI matrix cannot detect. Workspace [tool.uv.sources] overrides mean uv lock --check passes with a stale pin. The pin only fails when pip resolves against PyPI on a user's machine. Run the wheel-metadata check in Procedure D before pushing tags.
One multi-ref push silently skips releases. git push origin tag1 tag2 tag3 can start only the first workflow. Push each ref separately in a tight loop, then monitor the independent workflows concurrently.
Broken published wheels: remediation via PyPI yank (PEP 592). Yank via the PyPI web UI — twine does not support the yank operation. Yanked versions are skipped during resolution but remain installable when explicitly pinned. Always follow a bulk yank with a corrected patch release.
PyPI package names differ from directory names. unifi-network-mcp ≠ unifi-mcp-network. See Package Map.
Cross-package combined pytest run causes test configuration collision. Running uv run pytest packages/ apps/ causes pytest to load conflicting conftest files. Run per-package: cd apps/network && uv run pytest. CI workflows are already scoped this way.
Shared-package import blast radius is total. A single from unifi_network_mcp import inside packages/unifi-mcp-shared/ breaks all three app servers simultaneously at import time. The ImportError traceback names the shared package, masking the root cause. Run the grep from Procedure J-1 before every shared-package release.
Relay protocol drift is silent. The relay implementation files (packages/unifi-mcp-relay/src/unifi_mcp_relay/discovery.py and packages/unifi-mcp-relay/src/unifi_mcp_relay/protocol.py) have no import from unifi-mcp-shared — protocol changes fail silently until a real client exercises the changed path. Manual review on every protocol-touching PR is the only protection (see Procedure J-3).
Backwards-coupling enforcement. Import direction must stay: app servers → unifi-mcp-shared → unifi-core. Direct imports from app packages inside shared or core break at import time but no test catches it until the import is exercised. Grep before committing.
Docker builds must COPY transitive workspace dependencies, not just direct ones. packages/unifi-mcp-relay/Dockerfile once copied only its direct dependency's source, and broke when unifi-core became a transitive dependency of unifi-mcp-shared and was never added to the COPY steps. Any Dockerfile for a package with workspace dependencies must COPY the full transitive dependency source tree, not just direct dependencies — verify after any change to the shared/core dependency graph.
Diagnostics direct-import pattern. When debugging shared-package behavior from an app server, write diagnostic logic in the app server's namespace — do not import shared internals directly for inline inspection. Direct imports of shared internals count for cycle-checking.
Other versions1 project uses a version of this.
Word for word · 1
- sirkirby/unifi-mcp★ 978100% same
Edited · 0
None found.
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository16 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- docs/llms.txtllms.txt
- add-integration-api-toolSkill
- myco:api-endpoint-serializer-authoringSkill
- community-pr-batch-reviewSkill
- myco:community-pr-reviewSkill
- myco:extend-unifi-apiSkill
- myco:live-smoke-testingSkill
- myco:mcp-response-redaction-governanceSkill
- myco:runtime-bootstrap-and-test-isolationSkill
- myco:skill-reference-and-manifest-drift-maintenanceSkill
- test-pruningSkill
- myco:unifi-client-data-correctnessSkill
- code-reviewSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

