-
Notifications
You must be signed in to change notification settings - Fork 41
RELEASE
Maintainer guide. Users install by downloading /releases/latest/download/openstation.zip.
./bin/release.sh 1.2.0Bumps all four version locations, refreshes translation files (npm run i18n) — in that order, because wp i18n make-pot reads Project-Id-Version from the plugin header, so extracting first would stamp the catalogues with the previous version — drafts a = X.Y.Z = changelog block into readme.txt from GitHub's auto-generated release notes, then stops at a single interactive gate: it shows the block and requires an explicit y to continue — on every path, including --skip-changelog and resumed runs, and with a loud warning if the block is missing. Editing readme.txt while the prompt waits is supported: the bump commit picks up the file as saved, and the script re-prints the block if it changed. Answering n stops the release with nothing committed; fix the block and re-run — leftovers are tolerated, the draft merge is idempotent, and your edits survive, so the re-run lands straight back at the gate. If readme.txt already has a = X.Y.Z = block (hand-written, or committed by a feature PR), the draft still runs: existing entries are kept, only drafted bullets not already present verbatim are appended, and the appended ones are listed — watch for semantic duplicates. After confirmation it commits the bump and the language churn together, pushes to trunk, waits for CI green, tags, pushes the tag. Nothing is committed before the gate, so answering n leaves the bumped version files and refreshed catalogues in the working tree only. Aborts cleanly if you're not on trunk, local trunk is out of sync with origin, CI fails, or the working tree has changes beyond the script-owned files (languages/, readme.txt and the version files from an aborted attempt are fine; bump-version.sh rewrites them deterministically and they're swept into the bump commit). Resumable — re-running after a mid-flow failure picks up where it left off. The resume path requires the bump to be committed, not merely written: matching version strings in a dirty tree mean an earlier run stopped at the gate, so the re-run redoes the bump and commits it rather than tagging the pre-bump commit.
Flags:
-
--skip-i18n— skip the translation-file refresh. Use for hotfix releases where you don't want.pot/.po/.jsonchurn in the bump commit. -
--skip-changelog— skip drafting thereadme.txtchangelog block. Use when you've already hand-written it, or for hotfixes with nothing notable to log. The interactive changelog confirmation still runs; only the drafting step is skipped. -
--dry-run-changelog— print the changelog draft that would be inserted intoreadme.txt, then exit without modifying any files or pushing.
The tag push fires .github/workflows/release.yml, which builds and publishes a GitHub Release with openstation.zip attached, then — for stable tags only — deploys to WordPress.org.
Requires the gh CLI authenticated (gh auth status).
The last step of release.yml unpacks the zip and hands build/desktop-mode/ to 10up/action-wordpress-plugin-deploy, which commits it to SVN trunk and tags it. Pre-releases are skipped — the step is gated on the tag having no hyphen.
Two of the action's inputs default off a GitHub context that is only correct for a tag push, so the workflow sets both explicitly rather than leaving them implicit — SLUG (below) and VERSION, which the action derives from GITHUB_REF and which would resolve to the branch ref on a manual dispatch, producing an SVN tag called refs/heads/trunk. VERSION is exported from the version-gate step, so the deploy publishes the string that was just verified against all four version locations.
SLUG is set explicitly to desktop-mode and must stay that way. It is the published plugin's SVN path (plugins.svn.wordpress.org/desktop-mode/) and its install directory, so it is frozen for the same reason as every other desktop_mode_* value in AGENTS.md: changing it doesn't migrate anything, it points the deploy at a repository that doesn't exist and orphans every installed copy's update check. The action defaults SLUG to the GitHub repository name when unset — that default silently matched while the repo was named desktop-mode, and broke the moment it was renamed to openstation. Never rely on it.
Assets (banners, icons, screenshots) come from .wordpress-org/, the action's default ASSETS_DIR.
A tag push runs the workflow definition frozen into that tag's commit, so a deploy that failed for a workflow-level reason cannot be fixed by re-running it — the re-run replays the same broken definition. Dispatch the workflow from trunk instead, which runs the current definition against an existing tag:
gh workflow run release.yml --repo WordPress/openstation --ref trunk -f tag=v1.0.0The GitHub Release is left untouched: gh release create is not idempotent, so that step is gated on github.event_name == 'push' and skipped on dispatch. Everything else — checkout of the tag, the version gate, build, package, deploy — runs identically. The action itself is idempotent against SVN: a version already published is detected and skipped rather than re-committed.
Hyphenated versions publish as GitHub pre-releases, so /releases/latest keeps pointing at the last stable. The workflow detects the hyphen and sets --prerelease automatically:
./bin/release.sh 1.2.0-rc1| Tool | Purpose |
|---|---|
bin/bump-version.sh <version> |
Syncs package.json, package-lock.json, plugin header, OPENSTATION_VERSION, readme.txt Stable tag:, and packages/openstation-types/package.json. |
bin/package.sh |
Packages openstation.zip from HEAD + current built JS. The ZIP keeps the internal desktop-mode/ directory so WordPress.org upgrades and dependent plugins continue to resolve the established plugin slug. Derives the expected bundle list from vite.config.js TARGETS and ships each target's <fileBase>.min.js only — the unminified dev bundles (~4–5 MB) stay out of the zip; openstation_asset_suffix() falls back to .min on installs where they're absent, so a SCRIPT_DEBUG site degrades gracefully. Errors if any expected .min.js is missing under assets/js/, or if a stale gitignored .js not produced by any Vite target is left behind there. |
bin/release.sh <version> |
Full end-to-end release. |
release.yml — push: tags: v*
|
Build + publish the GitHub Release, then deploy stable tags to WordPress.org. |
Five places, kept in sync by bin/bump-version.sh:
-
package.json→"version"(andpackage-lock.jsonvianpm version) -
desktop-mode.php→ plugin headerVersion: -
desktop-mode.php→OPENSTATION_VERSIONconstant -
readme.txt→Stable tag:(wp.org rejects submissions when this drifts from the plugin headerVersion:) -
packages/openstation-types/package.json→"version"(vianpm version --prefix)
The release job re-reads all four at tag time and fails with a clear error if any doesn't match the tag. This catches "forgot to bump one".
Semver: vMAJOR.MINOR.PATCH.
- PATCH — bug fixes, no API changes.
- MINOR — new hooks / JS API / features. Backwards-compatible.
- MAJOR — breaking changes to documented PHP hooks, JS API, or the chromeless bridge protocol. Bump with care; plugins extend this shell.
Tags carry the v prefix (v1.2.0); package.json and the plugin header store the bare number.
For local testing without publishing:
npm run package # packages openstation.zip at the repo root (run npm run build first)The zip has the exact contents the workflow uploads.
Sibling plugins under extensions/ (e.g. desktop-mode-cron-manager,
desktop-mode-phpmyadmin) are packaged separately with:
./bin/package-extensions.sh # writes to dist/
./bin/package-extensions.sh /tmp/out # writes to a custom dir
./bin/package-extensions.sh /tmp/out desktop-mode-popup-siege
# packages one named extensionEach extension produces one <slug>.zip under dist/ (gitignored).
The script iterates every directory under extensions/ that has a
matching <slug>.php plugin file. For each, it:
- Runs any
bin/fetch-*.shscripts the extension ships with — these are idempotent vendor fetchers (e.g.desktop-mode-phpmyadmin'sbin/fetch-phpmyadmin.shpulls a SHA-256-pinned phpMyAdmin 5.2.3 zip intoassets/vendor/phpmyadmin/, the directory is gitignored). - Stages tracked + untracked-but-not-gitignored files via
git ls-files -co --exclude-standard, so packaging works on uncommitted work too. If an extension ships a.distignore, matching repository-only files are left out of the release artifact. - Splices any
assets/vendor/*working-tree content back in (vendors are gitignored by convention so end users still get a zip that activates without a setup step). - Round-trips through
tar+zipso file modes land at 0644 / 0755 — same trickbin/package.shuses for the main plugin.
Extensions are versioned independently from the parent plugin; bump the version in the extension's plugin header before re-packaging.
Version mismatch — tag 'X' vs package.json=Y header=Y OPENSTATION_VERSION=Y readme.txt Stable tag=Y
You pushed a tag without bumping first, or bumped but didn't push the bump commit before tagging. Fix locally, delete the broken tag (git push --delete origin vX.Y.Z), re-tag from the correct commit, push again.
bin/release.sh aborts with "working tree is dirty"
Commit or stash your in-progress work first. The script refuses to bundle unrelated changes into the bump commit.
Workflow succeeded but desktop.min.js is missing from the zip
Shouldn't happen — bin/package.sh errors out if the build artifacts aren't present, and the release job runs npm run build before it. If you see this, the build probably produced zero-byte files; check the Build step log.
Release created but with no notes / empty notes
--generate-notes pulls from merged PRs since the last tag. If there are none (first release, or only direct pushes to trunk), the notes will be sparse. Edit the Release in the GitHub UI after the fact.
Before cutting the first release, confirm:
- Repo Settings → Actions → General → Workflow permissions is set to Read and write permissions (needed for
gh release create). - The
v*tag pattern isn't blocked by a tag protection rule. - CI is passing on
trunk(the script enforces this before tagging).
This wiki is generated from the docs/ directory — edits made here are overwritten by the next sync.
To change a page, open a pull request against docs/.
Guides
- Development guide
- Releasing openstation
- Agents security model
- API Index
- Architecture
- Bridge protocol — wiring overview
- <os-*> component reference
- Native Desktop Host — Experimental
- Desktop themes
- Dock customization — two registries, one mental model
- The event-driven framework
- Files on the Desktop
- Folder sharing
- Getting Started
- Hooks Reference
- Icons
- JavaScript Reference
- The Living Tree — algorithm definition
- Mio
- Native Windows & Framework Interop
- Plugin compatibility layer
- Progressive Web App (PWA)
- Station Home
- Using openstation from your own plugin
Migration notes
- Migration: built-in activity channels move to the os/ namespace
- Migration: window, wallpaper and widget bundles load on demand
- Migration — the navigation model
- Migration: a native window's tabs move to the window chrome
All examples
- AI Agents — extend and invoke from a plugin
- wp.os.ai.ask() — programmatic AI Copilot
- Tune the AI model config
- Custom arrange-menu action
- Open a child window its owner can't cover
- Style a specific admin page inside the iframe
- Code Blue — register your plugin's log file
- Open a file in the Code editor (deep-link from any window)
- Connect to a window — title-bar button + iframe pub/sub
- Content changes — live-refresh every window listing your type
- Custom window chrome (Experimental)
- Register a custom unfocused-window effect
- Example: render a data table
- Real file storage — react to uploads, gate policy, share from PHP
- React to a window being set free onto the real desktop
- Cross-window devtools — instrumentation primitives
- Add a dock item with a badge
- Decorate the dock without forking the renderer
- Replace the dock rail entirely
- Retune the Drafts widget's AI writing assistant
- Gate OpenStation by role
- Iframe-initiated window opens
- Build a feed reader without the bookkeeping
- Inject data into openStationConfig
- Render a list without losing clicks — renderKeyedList()
- Example: layout primitives (body → panel → row → col)
- Use <os-*> components from a plugin that ships as a zip
- Restyle and drive Mio
- Add an action that works on a whole selection
- WP Explorer — custom post types and their folder
- Add an action button to a WP Explorer preview pane
- Example: native Posts window
- Example: native window with tabs
- Native windows
- Customize note → post conversion
- Send a notification
- OAuth relay — connect to an external service
- OS-file drop
- <os-flyout> — window-scoped sliding card
- Plugins window — extras
- Track who's around — wp.os.presence
- Example: progress bar
- PWA install — surface your own button
- React to window events
- Example: extend the Trash
- Register a slash-command
- Register a desktop theme from a plugin
- Register a game
- Example: register a desktop icon (Jorvy)
- Register a wallpaper
- Register a widget
- Related entities — extend the title bar's "Related" menu
- The native-window render ctx
- Programmatic folder sharing
- Share state across multi-bundle plugins — wp.os.createSharedStore()
- Example: loading spinner
- Add an opt-in card to Station Home
- Accept drops on your desktop icon
- Give a tile two icons, one per state
- Add a row to a window's ⋯ menu
- Example: window activity & the status ring
- Window controls
- Subscribe to window lifecycle events
- Window links — relate windows and restyle the ties (Experimental)
- Window loading state — spinner overlay & ready signal
- Show a banner at the top of a window
- Pulse a window's icon — Window.requestAttention()
- Register a custom window reveal
- Window slots
- Window themes
- Native window with bundle-bound config