Skip to content

Dev Release

Cut a tagged three-platform work-buddy release with preflight gates, native artifact acceptance, one draft-release aggregation job, and a human-only publish step.

Workflow name: dev-release

Execution: main

Slash command: /wb-dev-release

Override not allowed

Steps

# ID Name Type Depends on
1 assess Verify the repo is releasable (main, clean, synced, CI green) reasoning
2 version_gate Version, tag, and CHANGELOG agree reasoning assess
3 cross_repo Check companion-repo version pins (Obsidian plugin) reasoning version_gate
4 tag_push Tag and push (requires explicit user consent) reasoning cross_repo
5 watch_ci Watch the release workflow to completion reasoning tag_push
6 verify_draft Verify the draft release and its installer assets reasoning watch_ci
7 publish_handoff Hand the draft to the user to publish (never publish yourself) reasoning verify_draft
8 post_verify Verify the published release resolves publicly reasoning publish_handoff

Step instructions

assess

Reasoning step. Verify the repo is releasable:

git checkout main && git pull origin main
git status --short          # must be empty
git log origin/main..main   # must be empty (nothing unpushed)
gh run list --branch main --limit 3   # latest test run on main must be green

Advance with {"on_main": true, "clean": true, "synced": true, "ready": true} only when all hold. Any false value: fix first (commit or stash strays, push or pull, chase the red CI) and re-check. Set ready: false only if the user aborts the release.


Advance via wb_advance(workflow_run_id=..., step_result={...}). The parameter is step_result (not result): FastMCP silently drops unknown kwargs.

version_gate

Reasoning step. The intended tag, pyproject.toml, and the CHANGELOG must agree before anything is tagged:

uv run python packaging/version.py     # the version the release will carry
git tag -l "<version>"                 # must be EMPTY (tag not already used)
git ls-remote --tags origin "<version>"  # must be EMPTY too
grep -n "<version>" CHANGELOG.md       # an entry for this version must exist

The release CI hard-aborts when the tag differs from the pyproject version, so a mismatch here means the version bump has not landed yet: stop, land it through /wb-dev-pr, and restart this workflow. If the CHANGELOG lacks an entry, write one now (user-visible changes, not commit archaeology), land it the same way, and restart.

Advance with {"version": "<x.y.z>", "ready": true}.


Advance via wb_advance(workflow_run_id=..., step_result={...}). The parameter is step_result (not result): FastMCP silently drops unknown kwargs.

cross_repo

Reasoning step. work-buddy and its Obsidian plugin declare a two-way compatibility range; verify the release does not step outside it.

  • The enforced direction (blocking): work_buddy/obsidian/bridge.py defines PLUGIN_VERSION_MIN (inclusive) and PLUGIN_VERSION_MAX (exclusive), and require_available() raises outside that range. Fetch the plugin's latest released version (gh release view --repo KadenMc/obsidian-work-buddy --json tagName) and assert MIN <= released < MAX. Out of range means shipped work-buddy cannot talk to the shipped plugin: blocking, resolve before tagging.
  • The courtesy direction (advisory): the plugin's WB_VERSION_MAX (in its src/handlers.ts) is the first work-buddy version it has not been tested against. If the version being released is >= WB_VERSION_MAX, the plugin will log a cosmetic "outdated" warning against this release; work-buddy never reads that field, so this does not block. Note it and recommend a plugin-side bump plus plugin release as a follow-up.
  • The paired-constants rule: PLUGIN_VERSION_MAX (bridge.py) and WB_VERSION_MAX (plugin) each cap the OTHER project's version, so a major-line bump on either side requires moving the counterpart's constant in the same change. In particular, releasing a plugin version at or above PLUGIN_VERSION_MAX gets hard-rejected by the bridge until bridge.py moves.
  • The Thunderbird extension carries no version coupling to work-buddy; nothing to check.

Advance with {"plugin_in_range": true, "blocking": false, "notes": "<advisory findings, if any>"}.


Advance via wb_advance(workflow_run_id=..., step_result={...}). The parameter is step_result (not result): FastMCP silently drops unknown kwargs.

tag_push

Reasoning step. Pushing a tag starts the release machinery: ask the user for explicit consent first, naming the exact version. On yes:

git tag <version>
git push origin <version>

Advance with {"tagged": true, "tag": "<version>"}. If the user declines, advance with {"tagged": false, "tag": ""} and let the workflow end there.


Advance via wb_advance(workflow_run_id=..., step_result={...}). The parameter is step_result (not result): FastMCP silently drops unknown kwargs.

watch_ci

Reasoning step. The tag triggers the release workflow: Windows installer compilation; native Linux x86-64 and macOS arm64 tarball builds; downloaded-artifact install, launch, repair, and uninstall acceptance on fresh hosted runners; then one draft-release aggregation job. Watch it:

gh run list --workflow release.yml --limit 1
gh run watch <run-id> --exit-status   # or poll gh run view <run-id>

On success advance with {"ci_green": true}.

On failure: diagnose from gh run view <run-id> --log-failed. Do not leave a tag pointing at a commit whose release build failed: delete the remote tag (git push origin :refs/tags/<version> and git tag -d <version>), land the fix through /wb-dev-pr, and re-run this workflow from the top. Advance with {"ci_green": false, "notes": "<what failed and where the fix went>"}.


Advance via wb_advance(workflow_run_id=..., step_result={...}). The parameter is step_result (not result): FastMCP silently drops unknown kwargs.

verify_draft

Reasoning step. The CI creates a DRAFT release. Verify it before handing it to the user:

gh release view <version> --json isDraft,name,assets

Check: it is a draft; the name matches the version; and all accepted artifacts are present with plausible sizes: work-buddy-<version>-setup.exe, work-buddy-<version>-linux-x86_64.tar.gz, and work-buddy-<version>-macos-arm64.tar.gz. Write or polish the release notes body now (gh release edit <version> --notes-file ...): user-visible changes, installation instructions, supported architectures, and the unsigned macOS Gatekeeper expectation.

Advance with {"draft_ok": true, "assets": ["..."]}.


Advance via wb_advance(workflow_run_id=..., step_result={...}). The parameter is step_result (not result): FastMCP silently drops unknown kwargs.

publish_handoff

Reasoning step. Publishing is the user's act, always. Present the draft URL, the verified assets, and the release notes, then ask the user to review and publish it in the GitHub UI (or explicitly decline/postpone). Never run gh release edit --draft=false yourself, under any phrasing of user intent short of them publishing it with their own click.

Advance with {"published": true} once the user confirms it is live, or {"published": false, "notes": "postponed"} if they defer (the workflow then records the deferral and ends at post_verify with a no-op).


Advance via wb_advance(workflow_run_id=..., step_result={...}). The parameter is step_result (not result): FastMCP silently drops unknown kwargs.

post_verify

Reasoning step. Only meaningful after a publish:

gh release view --repo <owner>/work-buddy latest --json tagName   # resolves to this version
curl -sI https://github.com/<owner>/work-buddy/releases/latest | head -3

Confirm releases/latest resolves to the new tag (this is what the README's install link points at), and report the final summary: version, release URL, assets, and any advisory follow-ups from cross_repo (e.g. a recommended plugin-side bump). If publishing was deferred, state what remains and where the draft lives.


Advance via wb_advance(workflow_run_id=..., step_result={...}). The parameter is step_result (not result): FastMCP silently drops unknown kwargs.

Context

Cut a tagged work-buddy release: preflight gates (releasable main, version/tag/CHANGELOG agreement, companion-repo pin check), a consent-gated tag push, a watch of the release CI, verification of the draft release's installer assets, and an explicit human publish step.

Philosophy

Releases are rarer than commits, which makes a prose checklist worse, not better: nobody reliably remembers a multi-step ritual performed every few months. The same reasoning that produced the dev-pr workflow applies with more force here, plus one release-specific rule: the workflow produces and verifies a DRAFT, and publishing it is always the user's click, because publishing is an outward-facing act on the project's public face.

What this workflow is NOT

  • Not a version-bumping tool. Deciding the version (and landing the bump through /wb-dev-pr) happens before this workflow runs; the gates here verify agreement, they do not edit files.
  • Not a package publisher. It watches the repo's own release CI; it does not upload artifacts anywhere itself.
  • Not a substitute for the release CI's own guards (the tag-matches-pyproject assertion stays in the workflow file).