Release
pwrdrvr/openclaw-codex-app-server
Plan and publish a GitHub Release in a tag-driven repository.
A skill your agent uses when cutting, shipping, or announcing a Breeze RMM release — "cut a release", "ship vX.Y.Z", "release notes for X", "what do self-hosters need to know", "draft the release…
$ npx skills add LanternOps/breeze --skill release -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install LanternOps/breeze release --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/LanternOps/breeze.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/release .claude/skills/release && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "release" agent skill from https://github.com/LanternOps/breeze/tree/main/.claude/skills/release into .claude/skills/release/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "release", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/LanternOps/breeze/tree/main/.claude/skills/releaseType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add LanternOps/breeze --skill release -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install LanternOps/breeze release --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/LanternOps/breeze.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.claude/skills/release .agents/skills/release && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "release" agent skill from https://github.com/LanternOps/breeze/tree/main/.claude/skills/release into .agents/skills/release/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "release", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add LanternOps/breeze --skill release -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install LanternOps/breeze release --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/LanternOps/breeze.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.claude/skills/release .cursor/skills/release && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "release" agent skill from https://github.com/LanternOps/breeze/tree/main/.claude/skills/release into .cursor/skills/release/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "release", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/LanternOps/breeze.git --path .claude/skills/release--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add LanternOps/breeze --skill release -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install LanternOps/breeze release --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/LanternOps/breeze.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.claude/skills/release .gemini/skills/release && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "release" agent skill from https://github.com/LanternOps/breeze/tree/main/.claude/skills/release into .gemini/skills/release/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "release", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install LanternOps/breeze releaseInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add LanternOps/breeze --skill release -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/LanternOps/breeze.git skills-src && mkdir -p .github/skills && cp -r skills-src/.claude/skills/release .github/skills/release && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "release" agent skill from https://github.com/LanternOps/breeze/tree/main/.claude/skills/release into .github/skills/release/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "release", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add LanternOps/breeze --skill release -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install LanternOps/breeze release --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/LanternOps/breeze.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.claude/skills/release .opencode/skills/release && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "release" agent skill from https://github.com/LanternOps/breeze/tree/main/.claude/skills/release into .opencode/skills/release/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "release", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
releaseA skill your agent uses when cutting, shipping, or announcing a Breeze RMM release — "cut a release", "ship vX.Y.Z", "release notes for X", "what do self-hosters need to know", "draft the release…
Release is an agent skill from LanternOps/breeze. Use when cutting, shipping, or announcing a Breeze RMM release — "cut a release", "ship vX.Y.Z", "release notes for X", "what do self-hosters need to know", "draft the release announcement", "do the release write-up". Orchestrates the whole release-comms pass: a self-hoster-focused GitHub Release body (breaking changes, required env vars, migrations), then /update-breeze-release-notes (marketing site), /update-breeze-docs (technical docs), and a Discord announcement. Also covers the post-release deploy +…
Its SKILL.md is about 9.8k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Development, covering Changelog and release notes and Landing pages. It works with GitHub and Discord. The repository describes itself as: The open-source IT platform that comes with the workers. RMM + PSA in one system, with a governed AI operator built in. The licence is AGPL-3.0.
11 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 1f72bb7. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
ghdockergitsshdoctlcurlpnpmpsqlgonpxbashnpmFrom the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
breezermm.comAlso links to:
docs.breezermm.comFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Release loads about 9.8k tokens when it runs. Until then it costs about 144 tokens; SKILL.md has 4,604 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check noted patterns worth knowing about, such as sudo or a known installer.
s images (`grep '^BREEZE_API_IMAGE_REF=' .env`):ment:` block of their compose, not just `.env` — say so. If none: "**No new required environment variables.**"B via the `DATABASE_URL` in `/opt/breeze/.env`, **not** `doctl databases connection` (that points at an empty `defaultdbcp .env .env.bak-pre-$NEW && \BREEZE_VERSION=.*/BREEZE_VERSION=0.X.Y/' .env && \roplet> "cd /opt/breeze && set -a && . ./.env && set +a && \sh root@<droplet> 'set -a; . /opt/breeze/.env; set +a; psql "$DATABASE_URL" -f /tmp/promote.sql'Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from LanternOps/breeze at commit 1f72bb7, republished under its AGPL-3.0 licence (© LanternOps). 4,604 words, ~9,802 tokens.
.claude/skills/release/SKILL.md (or your agent's skills folder).This skill drives the communication and rollout pass for a release: the GitHub Release body (especially the self-hoster section), the marketing release notes, the technical docs, and the Discord announcement — plus the post-release deploy/promote steps the tag alone doesn't handle.
It assumes the code is already merged to main and green. Tagging is the trigger; this skill is about everything that surrounds it.
Infra specifics live in
internal/ops/release-infra.md(gitignored; read it from the MAIN checkout at~/breeze/internal/ops/release-infra.mdwhen working in a worktree). This skill refers to those values as$EU_IP,$US_IP,$EU_FW,$US_FW,$JUMP. Read that file FIRST when you reach Step 6 — do not guess or reconstruct the values.
.github/workflows/release.yml triggers on push: tags: v*. The tag's contents are whatever commit it points at, and the version is the tag minus the v. There is no VERSION file — cutting a release is pushing the tag.
generate_release_notes: true — i.e. an auto-generated commit list ("What's Changed" + Full Changelog). It does not write the curated Summary / Added / Improved / Fixed / Security / Self-Hosting sections. You add those afterward via gh release edit.v0.84.0; the agent version string, BREEZE_VERSION, and ldflags are 0.84.0.A tag off main HEAD ships all accumulated merged-but-unreleased work, not just "the latest thing", and auto-runs every pending migration on prod via autoMigrate on API boot. This has surprised us (the 0.83.1→0.83.2 "hotfix" carried 327 files + 10 migrations). So always look first:
PREV=v0.83.3 # the last released tag
NEW=v0.84.0 # the tag you're about to cut
git -C ~/breeze fetch --tags
git -C ~/breeze diff $PREV..HEAD --stat | tail -1 # total churn
git -C ~/breeze diff $PREV..HEAD --name-only -- apps/api/migrations/ # migrations that will auto-run on prod
git -C ~/breeze log --oneline $PREV..HEAD # everything that will shipmain HEAD.$PREV, cherry-pick only the fix. Do not tag off main HEAD if you don't intend to ship all of main.If migrations are listed, you're committing prod to those migrations the moment the new API boots. Read them; confirm there are no large-table rewrites/backfills before you proceed (call those out in the upgrade notes if present).
Smoke-test privileged migrations as a NON-SUPERUSER role (learned the hard way, v0.95.0→v0.95.1). CI and the local docker-compose test DB both migrate as the Postgres superuser (POSTGRES_USER=breeze), which silently passes statements a least-privilege role would be denied. Prod is DO-managed doadmin — NOT a superuser, so a migration that only a superuser can run passes every test and then crash-loops the API on deploy (this is exactly what took EU down on v0.95.0: ALTER FUNCTION ... OWNER TO <role> requires the new owner to hold CREATE on the schema; a superuser bypasses that check, doadmin does not → permission denied for schema public). So: any migration in the range that does CREATE ROLE, ALTER ... OWNER TO, GRANT/REVOKE on a schema, CREATE EXTENSION, or a SECURITY DEFINER function — run it against a non-superuser role before tagging. Two ways: pipe the migration to a real managed DB (or a droplet's doadmin) inside BEGIN; ...; ROLLBACK; with ON_ERROR_STOP=1, or SET ROLE <nosuperuser-createrole-role> on a local DB that already has the full schema. If it only works as a superuser, fix it forward (grant the owning role the privilege for the one statement, then revoke) before it reaches prod.
Every release ships the agent. release.yml builds and registers the full agent family on every tag, and the fleet promote is a standing rollout step — so "needs an agent release" is never a blocker or a special to-do, it is just what a release is. What an agent-side change does need is its real-host check done before the tag.
docs/testing/release-sweeps/*.md and its "Before the release cut" list, plus docs/release-notes/next-release-draft.md → "Release to-do". Any sweep fix PR named there merges before the tag.BLOCKED / PARTIAL for want of a live agent are run on the SSH lab rigs before tagging — not deferred to after the cut, and not left for a reporter to find. Use the non-prod rigs (the second Windows Server VM and the KIT lab-ubuntu-src VM are lab-enrolled against a local stack; the first Windows VM and the Ubuntu KVM rig are prod-enrolled — leave those alone unless the row is about prod upgrade behaviour). Recipe: pnpm wt-stack up at the commit to be tagged → cross-compile the agent family from that commit (GOOS=… CGO_ENABLED=0 go build -ldflags "-X main.version=<v>") → back up the rig's agent config → re-point server_url at the Mac's Tailscale/LAN address and re-enroll → run the rows → restore the config. Record results in the sweep doc.The post-upgrade splash (WhatsNewSplash, shown on dashboard load) renders an entry from
WHATS_NEW_ENTRIES in apps/web/src/lib/whatsNew.ts — a list bundled into the web image
at build time. No entry for the release = no splash after the upgrade, regardless of what
the server version says. This got skipped for 0.106.0 and 0.107.x, so the splash never fired
in prod even though the feature shipped in 0.105.0.
Ordering is the whole point: this is the one comms artifact that is pre-tag. The entry
must be merged to main before the tag is pushed, or the build won't contain it. Everything
else (release body, marketing notes, docs, Discord) happens after.
Add a new entry at the top of WHATS_NEW_ENTRIES (newest-first):
{
version: '0.X.Y', // bare semver — must equal the tag minus the 'v'
date: 'YYYY-MM-DD', // release date
title: '<one-line theme — same theme as the release body>',
highlights: [
// 2–5 short, user-facing bullets. Technician/MSP language, not commit
// subjects. Same audience as the marketing notes, tighter than both.
],
learnMoreUrl: 'https://breezermm.com/release-notes',
},$PREV..HEAD sweep as Step 1 — the 2–5 things a
technician would actually notice in the UI. Skip infra, deps, internal refactors. A
mostly-internal release can honestly say "hardening and reliability" — or skip the entry
entirely (skipping is a valid choice, just a deliberate one).cd apps/web && npx vitest run src/lib/whatsNewState.test.ts src/components/whatsNew/WhatsNewSplash.test.tsxlastSeenVersion floor in
localStorage is already baselined at the version they're running, so an entry versioned at
or below the shipped release will never auto-show for them. Version the entry as the next
release and fold the missed highlights into it.Pushing the tag kicks off release.yml, which is a long, multi-job build (~20–40 min): api/web/portal GHCR images, agent binaries for every platform, the signed Windows MSI, the notarized macOS agent/viewer/helper, watchdog + user-helper, and the signed release-artifact-manifest.json (+ .ed25519/.minisig) and checksums.txt. The GitHub Release is created by the final job — you cannot gh release edit the body until then, and you must not roll out until every artifact is present.
A partial or half-failed build can still publish a Release with missing binaries — installers and agent auto-update would then 404 in prod. So gate explicitly:
# Block until the run finishes (re-invokes you on completion):
RUN=$(gh run list --repo lanternops/breeze --workflow Release --branch $NEW --limit 1 -q '.[0].databaseId' --json databaseId)
gh run watch "$RUN" --repo lanternops/breeze --exit-status
# Then confirm success AND a full asset set — compare the count to the previous release:
gh release view $NEW --repo lanternops/breeze --json assets -q '.assets | length'
gh release view $PREV --repo lanternops/breeze --json assets -q '.assets | length' # expected baseline (~40)
gh release view $NEW --repo lanternops/breeze --json assets -q '.assets[].name' | sortBefore proceeding, verify the new release has the same asset families as the prior one — the macOS .pkg/.dmg/.app.tar.gz, the viewer/helper installers, the -unsigned BYO-signing inputs, and release-artifact-manifest.json + .ed25519 + .minisig + checksums.txt. A short asset list = signing or notarization failed midway.
gh run rerun "$RUN" --failed) and re-watch. Don't hand-publish a release off a red build, and don't roll out.gh run watch can exit non-zero while the run is still in progress (seen 2026-07-02) — before treating a watch failure as a build failure, check gh run view $RUN --json status: in_progress means keep waiting (a status-polling loop is more reliable than gh run watch).Since the installer-trust fork, the public release must contain nothing abuse-usable, and the signed hosted agent build comes from a separate private lane. Both are release-blocking; the tag lands as a draft (RELEASE_DRAFT_FIRST=true) and stays one until the hosted cutover completes.
1. Verify the public draft's assets — bytes and manifest, not job color:
edition=self-host in release-artifact-manifest.json; zero edition=hosted entries). Check the actual bytes: PE security-directory size 0 on the exe, no DigitalSignature stream in the MSI, and the -unsigned asset byte-identical (cmp) to its non-suffixed twin.checksums.txt against downloaded assets.2. Dispatch the private hosted signing lane. Repo name, dispatch command, edition choice (gap vs strict), and token gotchas live in internal/ops/release-infra.md ("Hosted signing lane") — read it, don't reconstruct. Key facts:
Read docs/release-notes/next-release-draft.md FIRST. It is the running
scratch list where operator-visible details get written down at the moment
they're introduced — new env vars, new log lines, new metrics, behaviour changes
— rather than being reconstructed from a commit subject weeks later. Fold each
entry into the body below (most of it belongs in Self-Hosting / Upgrade Notes),
then clear the file in the same PR that publishes the release, so a stale
entry can't be shipped twice. It is a supplement to the git history, not a
replacement: still do the $PREV..$NEW sweep, because anything nobody thought to
write down only exists there.
Release content otherwise comes from merged PRs and git history only — never blog posts, design docs, or product docs (those describe planned, not shipped, work).
git -C ~/breeze log --oneline $PREV..$NEW
gh pr view <N> --repo lanternops/breeze --json title,body
git -C ~/breeze log --format="%B" -1 <SHA> # expand a squash-merge bodySkip from user-facing notes: chore(deps):, chore:, ci:/fix(ci):, internal refactor:, internal docs:/test:. Keep anything a self-hoster or operator would notice.
The GitHub Release is the operator/self-hoster source of truth. Model the body on the most recent release for structure and tone:
gh release view $PREV --repo lanternops/breeze # reference templateStructure (omit empty sections):
Breeze RMM **vX.Y.Z** — one-line theme of the release.
## Summary
- **Area** — what shipped, in operator language (#PR).
## Added
- **Feature** — what it does for the user (#PR)
## Improved
- ... (#PR)
## Fixed
- ... (#PR)
## Security
- Hardening summary; "Self-hosters are encouraged to upgrade." (#PRs)
## Self-Hosting / Upgrade Notes
<the most important section — see below>Self-hosters run their own droplets and read this section to decide whether an upgrade is safe and what they must touch. Always answer these four questions explicitly, even when the answer is "nothing required" — silence reads as "I don't know":
Upgrade command. Give both paths, because the right one depends on how the install pins its images (grep '^BREEZE_API_IMAGE_REF=' .env):
@sha256:… — every guided-setup.sh install for v0.112.0+): bash guided-setup.sh --upgrade 0.X.Y (fetch the current script from main first). It verifies the signed image inventory and rewrites BREEZE_VERSION and all four BREEZE_*_IMAGE_REF digests together, then pulls and restarts.:${BREEZE_VERSION}): bump BREEZE_VERSION, then docker compose pull api web portal && docker compose up -d (and pnpm install if they build from source).Link the Upgrade Guide. Never write "set BREEZE_VERSION, then pull" as the only upgrade line. On a digest-pinned install that re-pulls the images already running, so nothing upgrades. Until v0.118.2, /health also reported the edited version because Compose copied BREEZE_VERSION into APP_VERSION (#7024), so operators believed they had upgraded. Only the web footer showed the real version. Every release note from v0.112.0 to v0.118.2 gave that line, and self-hosters were stranded on 0.115→0.116 and 0.117→0.118.2. Keep portal in the tag-form pull list: it is a separate container and was left behind for 11 days (stuck on 0.94.0 while api/web ran 0.98.1) when the line pulled only api web.
Database / migrations. State the count and that they're idempotent and auto-apply on boot via autoMigrate (unless AUTO_MIGRATE=false). Explicitly flag any large-table rewrite or backfill — that's the difference between a 2-second upgrade and a stalled boot. If nothing: "Database — nothing required."
New required environment variables. Anything the config validator now refuses to boot without (e.g. past examples: RELEASE_ARTIFACT_MANIFEST_PUBLIC_KEYS, IS_HOSTED). A new required env var must also be mapped in the api/web environment: block of their compose, not just .env — say so. If none: "No new required environment variables."
Behavior changes & feature flags. Anything whose default changed (call out grandfathering of existing orgs), and any feature gated behind a flag (e.g. PUBLIC_ENABLE_EDR_INTEGRATIONS) — name the flag and its default.
End with the compare link: **Full Changelog:** https://github.com/LanternOps/breeze/compare/$PREV...$NEW
If there are any, set the tone up front (a bolded ⚠️ Breaking changes callout near the top and its own bullet in upgrade notes). Be concrete: what breaks, who is affected, what action they must take, and whether there's a grandfather/opt-out. If there are none, say "No breaking changes." somewhere in the upgrade notes — self-hosters look for that reassurance. Most Breeze releases are non-breaking; behavior-default changes (like the PAM UAC opt-in flip) are the usual "soft" breaking case and must be flagged.
CI already created the release with the auto-generated "What's Changed". Preserve that — append your curated sections above it rather than clobbering:
gh release view $NEW --repo lanternops/breeze --json body -q .body > /tmp/auto-notes.md
# Write curated sections to /tmp/curated.md, then:
cat /tmp/curated.md /tmp/auto-notes.md > /tmp/release-body.md
gh release edit $NEW --repo lanternops/breeze --notes-file /tmp/release-body.mdInvoke /update-breeze-release-notes. That skill owns the breezermm.com content schema, the dev→user language translation, and the categories grouping for major releases. Don't duplicate its rules here — hand it the version range and let it work. (Its notes are for MSP owners/technicians; tone is lighter and more marketing than the GitHub body.) Remember the site only publishes via its deploy script (npm run deploy:notify) — a commit alone changes nothing live.
Invoke /update-breeze-docs in diff mode for the version range (e.g. "update docs since $PREV"). It maps changed code → affected docs pages, filters by audience, and bumps its review tracker. Required env vars, new feature workflows, and behavior changes from this release should land in the Starlight docs.
Draft a short, friendly announcement for the community Discord. Keep it scannable — highlights, not the full changelog — and always include the self-hoster upgrade line and any breaking/behavior change. Show it to Todd before posting; don't post it yourself unless asked.
Template:
🚀 **Breeze vX.Y.Z is out**
<one-line theme>
**Highlights**
• <feature 1>
• <feature 2>
• <feature 3>
• <security note, if any>
**Self-hosting:** <one line — "`guided-setup.sh --upgrade X.Y.Z` (or bump `BREEZE_VERSION` + pull on tag-form installs), N idempotent migrations, no new env vars" OR the specific action needed>
<⚠️ one line if there's a breaking/behavior-default change>
📝 Full notes: https://github.com/LanternOps/breeze/releases/tag/vX.Y.ZTone: operator-to-operator, not corporate. Lead with what people can do now. If a release is mostly internal/security, say so plainly ("mostly hardening + reliability — recommended upgrade").
The tag built images and registered binaries; it does not deploy prod or update the agent fleet on its own. The intent is for cutting a release to flow straight into rollout — but always confirm with Todd before touching prod, then proceed. Three parts, per region: back up the DB → deploy the droplet → (later) promote the agent fleet.
Read internal/ops/release-infra.md now (main checkout: ~/breeze/internal/ops/release-infra.md) for the real droplet IPs ($EU_IP/$US_IP), firewall IDs ($EU_FW/$US_FW), and jump-host name ($JUMP) used below.
Two access paths, in order of preference:
ssh -J $JUMP root@<ip> — works when the bastion is up and you can complete its browser re-auth; the bastion's SSH is posture-gated with an interactive check, so headless sessions usually cannot use it — if it prompts with a login URL or hangs, don't burn time, fall back to (2).MYIP=$(curl -s4 ifconfig.me)
for fw in $EU_FW $US_FW; do
doctl compute firewall add-rules $fw --inbound-rules "protocol:tcp,ports:22,address:$MYIP/32"
done
# ... do the rollout via ssh root@$EU_IP / ssh root@$US_IP ...
# then restore:
for fw in $EU_FW $US_FW; do
doctl compute firewall remove-rules $fw --inbound-rules "protocol:tcp,ports:22,address:$MYIP/32"
doneadd-rules/remove-rules are additive/surgical — they won't touch the existing allowlisted IP. Remove your exception once both regions are healthy and promoted (an RMM's prod SSH shouldn't stay open to a residential IP). If the fleet-promote step is deferred pending Todd's go-ahead, remove the exception anyway and re-add it when he approves — it's two commands.
Migrations auto-run the moment the new API boots (autoMigrate), so the only safe rollback artifact is a snapshot taken immediately before deploying that region. Prod uses DigitalOcean managed Postgres (no breeze-postgres container on the droplet) — back it up with the on-demand encrypted off-region wrapper, which sources backup.env, pg_dump -Fcs the live DB, gpg-encrypts it, and uploads off-region:
# On the region's droplet (do this right before you deploy that region):
ssh root@<droplet> "bash /opt/breeze-backup/run-backup.sh"/opt/breeze-backup/run-backup.sh (the same cron wrapper) — it set -a; . backup.env then runs the encrypted-offsite script. Don't call the inner script directly (it needs the env sourced); dumps are timestamp-named (db_YYYYMMDD_HHMMSS.dump).defaultdb gotcha: the backup must target the live breeze DB via the DATABASE_URL in /opt/breeze/.env, not doctl databases connection (that points at an empty defaultdb, ~4K dump). backup.env is already configured for the right DB — just run the wrapper.Database backup complete: …/db_*.dump (NNM) and ends with Off-region encrypted backup complete. Real sizes are tens-to-hundreds of MB — per-region baselines are in internal/ops/release-infra.md. A ~4K dump means you hit the defaultdb trap; stop and fix backup.env.internal/ops/off-region-backup-runbook.md, gitignored.)Do this per region, in the night-first order below — back up region 1, deploy region 1, verify, then back up region 2, deploy region 2.
Two parts after backup: deploy the droplets, then promote the agent fleet.
EU and US are separate droplets/DBs. Deploy whichever region is in the middle of its local night first, verify it, then do the other. The point is to let any bad upgrade surface against the smallest live audience.
/health 200 and spot-check → only then move to the other region.Auto-detect the night region (DST-aware — EU droplet ≈ Amsterdam time, US ≈ NYC time). Run this and deploy the region it prints first:
# Picks whichever region's local time is closest to 3 AM (deepest into the night).
eu=$(TZ="Europe/Amsterdam" date +%H); us=$(TZ="America/New_York" date +%H)
# circular distance from 03:00, in hours
d(){ h=$((10#$1)); a=$(( (h-3+24)%24 )); b=$(( (3-h+24)%24 )); echo $(( a<b ? a : b )); }
if [ "$(d "$eu")" -le "$(d "$us")" ]; then
echo "Deploy EU first (Amsterdam ${eu}:00, NYC ${us}:00), then US"
else
echo "Deploy US first (NYC ${us}:00, Amsterdam ${eu}:00), then EU"
fiPer droplet:
ssh root@<droplet> "cd /opt/breeze && \
cp .env .env.bak-pre-$NEW && \
sed -i 's/^BREEZE_VERSION=.*/BREEZE_VERSION=0.X.Y/' .env && \
docker compose pull api web portal && \
docker compose up -d binaries-init api web portal && \
docker image prune -af --filter 'until=168h' && \
docker builder prune -af"
# then verify:
curl -sf https://<region>.2breeze.app/health # 200 = healthy; check "version" in the JSON
# and confirm migrations applied cleanly:
ssh root@<droplet> "docker logs breeze-api 2>&1 | grep -aE 'auto-migrate' | tail -5"
# expect "[auto-migrate] Applied N migration(s)" and the unprivileged app-user line
# the two prune lines keep stale release images from filling the root disk (US hit 100% twice in Sept 2026)Then assert version parity across EVERY first-party container — /health does NOT cover this.
/health is served by the API, so it reports the new version even when a sibling container was never rolled. Don't eyeball the service list; enumerate what is actually running and compare each tag to BREEZE_VERSION:
ssh root@<droplet> "cd /opt/breeze && set -a && . ./.env && set +a && \
docker ps -a --format '{{.Names}}\t{{.Image}}' | grep 'ghcr.io/lanternops/breeze/' | \
while IFS=\$'\t' read -r n i; do t=\${i##*:}; \
[ \"\$t\" = \"\$BREEZE_VERSION\" ] && echo \"OK \$n \$t\" || echo \"SKEW \$n \$t (expected \$BREEZE_VERSION)\"; done"
# every line must be OK. Any SKEW = that service was not rolled — pull/up it and re-check.
# Today that is api, web, portal, binaries-init. It self-updates as services are added,
# which is the point: the hand-maintained list above is what failed before.Why this check exists. The deploy line names services explicitly (rather than a bare docker compose pull && up -d) for two real reasons: billing is built from a local breeze-billing:local image that has no registry to pull from, and a bare up -d would bounce caddy/redis/tunnel unnecessarily. But that makes the service list a hand-maintained list that goes stale the moment a new first-party service is added — and nothing else catches it. portal was added in v0.94.0, never made it into the deploy line, and silently sat on 0.94.0 through five releases while /health cheerfully reported 0.98.1; a portal fix shipped in v0.97.0 was invisible in production for 11 days until a customer-facing proposal link surfaced it. Watchtower is not a safety net here: it runs with WATCHTOWER_LABEL_ENABLE=true and no service carries the enable label, so it updates nothing. The parity check is the backstop that does not depend on anyone remembering to update a list.
autoMigrate applies each migration file in its own transaction and records success per file. If migration #7 of 10 fails, files #1–6 have already committed. Rolling BREEZE_VERSION back to $PREV restores the old code but the DB is now on a newer-than-$PREV schema, and the old code can choke on it. This bit us on v0.95.0: the auth-epochs migration committed a NOT NULL-no-default column (refresh_token_families.absolute_expires_at) before a later migration failed; after rolling back, the old version could not INSERT a refresh-token family (the column it doesn't know about is NOT NULL), so new logins silently failed while /health stayed green.
So after any rollback that followed a mid-set failure:
/health is not proof of recovery. Test the real write paths — especially minting a refresh-token family (login) — against the actual DB (INSERT ... in a BEGIN; ... ROLLBACK;).NOT NULL columns with no default, or new CHECK constraints, added by the committed-but-newer migrations on tables the old version writes (users, refresh_token_families, oauth_*, devices, audit_logs).ALTER COLUMN <col> SET DEFAULT <sane value> (keeps NOT NULL, lets the old code insert) is the lightest fix; it's inert on the new version (which sets the value explicitly). Note the divergence and drop the DEFAULT once you're forward again so the schema matches a fresh install.Prod sets AGENT_AUTO_PROMOTE=false, so deploying registers the new binaries in agent_versions (is_latest stays on the prior version) and devices don't update until you promote. Promote with a direct DB row change — this is the method precisely because it does not require an MFA session. The API /promote endpoint is gated on a platform-admin MFA session (which we can't drive headlessly), so editing agent_versions directly is the authorized, MFA-free path. Confirm with Todd first, then run it per region (US and EU have separate DBs — promote each).
The droplets have psql installed; run against the region's live DB:
ssh root@<droplet> 'set -a; . /opt/breeze/.env; set +a; psql "$DATABASE_URL" -f /tmp/promote.sql'The promotion is slot-aware: it's per (component, platform, architecture) and must only demote slots the target version actually covers — a naive SET is_latest=false WHERE is_latest=true strands any platform/arch the new version lacks. Correct SQL, single transaction:
BEGIN;
UPDATE agent_versions a SET is_latest=false
WHERE a.is_latest=true
AND EXISTS (SELECT 1 FROM agent_versions t
WHERE t.version='0.X.Y'
AND t.component=a.component
AND t.platform=a.platform
AND t.architecture=a.architecture);
UPDATE agent_versions SET is_latest=true WHERE version='0.X.Y';
COMMIT;Caveats:
component in the SQL above promotes everything (agent + helper + watchdog + user-helper). Narrow with AND component='agent' etc. if doing a partial roll.audit_logs row the API endpoint would write. The table is hash-chained — do not hand-forge an audit row (a bad row corrupts the chain). Just note the audit gap; re-running the real /promote endpoint later is idempotent on the version rows and writes the audit entry.select component,platform,architecture,version,is_latest from agent_versions where is_latest and edition='<BINARY_EDITION of this droplet>' order by 1,2,3; — expect the new version across all covered slots, and no slot left with zero is_latest.helper rows for windows/amd64, macos/amd64, macos/arm64, linux/amd64. Zero helper rows means BINARY_SOURCE=local sync did not see /data/binaries/helper (#6872) — Assist bootstrap is down until it does.Rolling the fleet is a user-facing change — get Todd's go-ahead before promoting, and don't promote until both regions are deployed and healthy.
Security releases: publish the GHSA advisory after rollout completes, never before (fix → quiet release → roll out → then publish). Internal cadence docs: internal/security-disclosure-policy.md (the decision doc), internal/security-advisory-queue.md (what's owed), internal/security-hardening-tracker.md.
Not a "security release only" step. Run it before writing the release body, because a pending advisory changes how this release's notes are worded.
gh api repos/lanternops/breeze/security-advisories \
--jq '.[] | select(.state=="draft") | "DRAFT \(.ghsa_id) patched=\(.vulnerabilities[0].patched_versions // "?") — \(.summary)"'GATE — this release does not proceed if a draft advisory's patched_versions has already rolled
out. Publish it first (internal/security-advisory-queue.md has the exact commands). Once a fix
ships, it is visible in the diff and the release notes; the advisory is the only thing that turns it
into a notification. Holding it protects nobody and leaves self-hosters unaware they're exposed.
This gate exists because the old trailing checkbox ("GHSA published after rollout") never once fired: it sat last, after manual go-ahead-gated steps, so the release session always ended before rollout finished — and nothing persisted the debt afterward. Two High advisories from v0.69.0 sat unpublished for 2.5 months and 36 releases. Binding the check to the next release works because releases reliably recur; "after rollout" is a moment nobody revisits.
Split by urgency — one bullet in the first section carries more signal than a paragraph buried in forty:
### Security — action required
Self-hosted operators on <affected range> should upgrade immediately. <what an attacker could DO>.
(GHSA-xxxx, #NNNN)
### Security — hardening
<routine defense-in-depth, batched, no urgency language>Say what an attacker could do, not what the code now does — "could execute arbitrary code as
root on macOS agents with auto-update enabled", not "now verifies the .pkg against the manifest".
Name the affected version range; self-hosters do not know whether they're affected. Full taxonomy
and per-severity clocks: internal/security-disclosure-policy.md.
gh api ... select(.state=="draft")) — any draft whose patched version already rolled out is published before this release proceedsdocs/release-notes/next-release-draft.md read and folded into the body — then clearedgit diff $PREV..HEAD --stat + migrations reviewed — blast radius understoodWHATS_NEW_ENTRIES (apps/web/src/lib/whatsNew.ts) and merged to main BEFORE the tag — version = bare semver of this release (or deliberately skipped for an internal-only release)edition=self-host, checksums spot-checkedinternal/ops/release-infra.md), with Todd's explicit go; draft stays unpublished until the droplet flip completes/update-breeze-release-notes (marketing site) — published via the site's deploy script, not just committed/update-breeze-docs (technical docs)defaultdb 4K trap)/health greeninternal/security-advisory-queue.md moved to Published. If rollout finishes after this session ends, the queue row is what carries the debt forward; the next release's pre-flight gate will block on it.v0.84.0; everywhere else 0.84.0 (bare semver).CHANGELOG.md — it's abandoned at 0.67.1. GitHub Releases are the changelog.gh repo is lanternops/breeze (canonical remote is LanternOps/breeze). Push and PR there.gh release view <prev-tag> is the best style guide for the body. Match its section order and tone./update-breeze-release-notes) are for MSP buyers (benefit-led). Don't copy-paste between them.internal/ops/release-infra.md (gitignored) — this skill is committed to a public repo. Keep it that way when editing.© LanternOps, AGPL-3.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in .claude/skills/release of LanternOps/breeze.
Open the folder on GitHubat commit 1f72bb7
Release next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Release this skillLanternOps/breeze | 130 | — | ~9.8k | Automated safety check: Notes | AGPL-3.0 | |
| Releasepwrdrvr/openclaw-codex-app-server | 265 | — | ~2.2k | Automated safety check: Pass | MIT | |
| Publish Releasearietan/lite-edit | 168 | — | ~656 | Automated safety check: Pass | MIT | |
| Writing Release NotesGoneTone/genshin-impact-wish-gacha-analyzer | 152 | — | ~1.6k | Automated safety check: Pass | MIT | |
| Release Discord Postnevalang/neva | 1.1k | — | ~500 | Automated safety check: Pass | MIT | |
| Subwave Discord Releaseperminder-klair/subwave | 1.4k | — | ~2.3k | Automated safety check: Pass | MIT |
pwrdrvr/openclaw-codex-app-server
Plan and publish a GitHub Release in a tag-driven repository.
arietan/lite-edit
Publish a new LiteEdit release to GitHub — commit code, bump version, write changelog, create GitHub release, and update the landing page.
GoneTone/genshin-impact-wish-gacha-analyzer
A skill your agent uses when drafting GitHub release notes or changelog content for this project (Genshin Impact Wish Gacha Analyzer).
nevalang/neva
Create a Discord-ready Neva release announcement from a GitHub release payload.
perminder-klair/subwave
Draft a Discord release announcement for SUB/WAVE from a release PR, tag, or version number.
rvdbreemen/OTGW-firmware
Publish an OTGW-firmware beta prerelease — bump VERSIONPRERELEASE, push to otgw-1.x.x, tag, and let CI build + publish the GitHub prerelease
LanternOps/breeze
Quick reference for the Breeze RMM Go agent architecture, commands, configuration, build process, and data flows.
LanternOps/breeze
A skill your agent uses when debugging agent issues, investigating agent errors, checking agent connectivity, or reviewing agent diagnostic logs.
LanternOps/breeze
Quick reference for the Breeze RMM AI Agent system architecture, MCP tools, streaming chat, cost tracking, guardrails, and MCP server.
LanternOps/breeze
Quick reference for the Breeze Helper Tauri desktop app — architecture, Rust backend commands, React frontend, config files, IPC with the Go agent, helper chat API routes, tool approval flow, and…
LanternOps/breeze
A skill your agent uses when running a broad manual/AI-driven end-to-end verification of Breeze RMM across many merged PRs or commits — "test everything since the last release", release-readiness…
LanternOps/breeze
A skill your agent uses when orchestrating Breeze implementation work from this seat — dispatching waves or issue fixes to background sessions, deciding whether an open PR gets merged, handling a…
Categories
A skill your agent uses when cutting, shipping, or announcing a Breeze RMM release — "cut a release", "ship vX.Y.Z", "release notes for X", "what do self-hosters need to know", "draft the release…. Release is an agent skill from LanternOps/breeze.Z", "release notes for X", "what do self-hosters need to know", "draft the release announcement", "do the release write-up".
Release fits situations like: announcing a Breeze RMM release — cut a release; release notes for X; what do self-hosters need to know; draft the release announcement.
Run `npx skills add LanternOps/breeze --skill release -a claude-code`. Or copy the skill folder (.claude/skills/release in LanternOps/breeze) into .claude/skills/release in your project. Claude Code loads it when a task matches its description.
Run `npx skills add LanternOps/breeze --skill release -a codex`. Or copy the skill folder (.claude/skills/release in LanternOps/breeze) into .agents/skills/release in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add LanternOps/breeze --skill release -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/release, .gemini/skills/release, .github/skills/release and .opencode/skills/release in your project.
Going by SKILL.md and its folder, Release needs the command-line tools its instructions call (gh, docker, git, ssh, doctl and curl). Our summary lists: Node.js; Docker.
SKILL.md names 2 domains. In commands or code: breezermm.com; the agent is likely to contact it when it follows the instructions. As links in the text: docs.breezermm.com. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found notes only (mentions a .env file), nothing it rates as a warning. It is not a guarantee. Review the folder before installing.
Release is published under the AGPL-3.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 9.8k tokens (SKILL.md is roughly 39k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Release: Release (pwrdrvr/openclaw-codex-app-server, 265 stars), Publish Release (arietan/lite-edit, 168 stars), Writing Release Notes (GoneTone/genshin-impact-wish-gacha-analyzer, 152 stars) and Release Discord Post (nevalang/neva, 1.1k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
LanternOps (a GitHub organization) maintains it in LanternOps/breeze, which has 130 GitHub stars. The repository holds 14 skills in this directory. The repository was last updated on October 7, 2026.
Source: LanternOps/breeze on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.