Publishing with Release Please and Moonrepo on GitHub Actions
Source of Truth
Read references/release-publish-channel-and-metadata-plan.md before implementing this pattern. It contains the full contract, test matrix, and implementation order.
Terminology is strict:
channelmeans only the distribution channel:nextorlatest.release_tagmeans only the actual GitHub Release tag receiving assets.source_shameans the immutable commit that publication verifies and checks out.- Moon project
sourceis the Release Please package/manifest key and version-source location. - Moon project
idis the Release Pleasecomponentand human-facing release-tag prefix.
Do not reuse older guidance that dispatches computed versions or release tags. Publish metadata must be computed once from the verified checked-out source.
Architecture
textpush: main | release/* | v release.yml branch ownership gates | +-- release branch root ----------------> stop +-- hotfix merge-back on main ----------> stop +-- main-owned commit ------------------> normal RP config +-- release-owned commit ---------------> hotfix RP config | v Release Please | +--------------------------+------------------+ | | releases_created=true releases_created=false channel=latest channel=next | | +--------------------------+------------------+ | get-publish-matrix selects target IDs | dispatch target/channel/source_sha/source_branch | v publish.yml | validate source, checkout immutable source_sha | resolve metadata from checked-out Moon project | create/find release, run Moon publish task
prs_created is reporting data only. It MUST NOT suppress channel=next publishing.
Ownership Boundaries
release.yml
MUST own:
- branch ownership and duplicate-processing gates;
- normal versus hotfix Release Please config selection;
- explicit Release Please target branch on
release/*; - channel selection from
releases_created; - affected publish-target selection through
get-publish-matrix; - immutable source identity in repository dispatch;
- one dispatch per selected Moon project.
MUST NOT:
- compute artifact versions;
- compute GitHub release tags;
- publish artifacts;
- dispatch a version or release tag.
publish.yml
MUST own:
workflow_dispatchand trustedrepository_dispatchentry points;- input/payload normalization;
- dispatch actor validation;
- source branch and SHA validation;
- exact immutable checkout;
- publish metadata computation from checked-out source;
- prerelease creation and stable release validation;
- publish environment export;
moon run "${PUBLISH_TARGET}:publish" --force.
MUST NOT:
- query affected projects;
- independently choose normal versus hotfix policy outside verified branch rules;
- accept a caller-computed version or GitHub release tag.
Moon publish tasks
MUST consume exact environment values and perform package-specific publication only:
textPUBLISH_TARGET Moon project ID PUBLISH_CHANNEL next | latest PUBLISH_VERSION Computed artifact semantic version PUBLISH_RELEASE_TAG Computed GitHub Release tag PUBLISH_REF Verified immutable source SHA
Tasks MUST NOT derive versions or release tags.
Dispatch Contracts
Repository dispatch:
json{ "event_type": "publish-package", "client_payload": { "target": "zellij-plugin", "channel": "next", "source_sha": "abc123", "source_branch": "release/0.1" } }
No version, release_tag, overloaded tag, or duplicate mode field is allowed.
Manual publishing belongs on publish.yml:
yamlworkflow_dispatch: inputs: target: type: string required: true channel: type: choice required: true default: next options: [next, latest]
Manual source identity comes from github.sha and github.ref_name, then passes the same branch and reachability checks as repository dispatch.
Channel Migration
Apply this rename across workflows, matrices, summaries, tests, and project tasks:
textinputs.tag -> inputs.channel client_payload.tag -> client_payload.channel PUBLISH_TAG -> PUBLISH_CHANNEL
Do not rename native external CLI flags:
bashnpm publish --tag "${PUBLISH_CHANNEL}"
Release Please Synchronization
.github/actions/sync-moon-to-release-please/action.sh MUST discover publishable Moon projects without requiring package.json.
Discovery:
- Run
moon query projects. - Retain projects with a
publishtask. - Use
project.sourceas the Release Please package and manifest key. - Set
componentto the exact Moon project ID. - If
<source>/package.jsonexists, read.versionand userelease-type: node. - Else if
<source>/Cargo.tomlexists, read package version through Cargo metadata and userelease-type: rust. - Otherwise fail with an unsupported version-source error.
Synchronization MUST preserve existing manifest versions, seed new entries from source versions, and write the same source-path keys and Moon-ID component values to normal and hotfix configs. Do not guess Moon IDs from package names or add a static mapping file.
Release Please package keys MUST remain repository paths. Setting component changes human-facing component names and tag prefixes without breaking package discovery:
json"pkgs/plugins/zellij-plugin": { "component": "zellij-plugin", "release-type": "rust" }
Keep version-source discovery expandable with a documented case statement or equivalent plugin boundary.
Publish Target Selection
Keep .github/tasks/get-publish-matrix as the only affected-project selector.
It MUST:
- Run
MOON_BASE=... MOON_HEAD=... moon query projects --affected. - Retain projects containing a
publishtask. - Return sorted Moon project IDs only:
json[ { "target": "docs" }, { "target": "zellij-plugin" } ]
It MUST NOT emit mode, tag, channel, version, release_tag, or source_sha. release.yml adds channel and source identity when dispatching.
Publish Metadata Resolver
Use one deployment helper, preferably:
textpkgs/tools/deployment/resolve-publish-metadata
Remove generate-publish-git-tag after migrating callers. Two helpers would duplicate version logic.
Interface:
textresolve-publish-metadata <target> <channel> <source-branch> <run-attempt>
Return machine-readable metadata, preferably JSON:
json{ "target": "zellij-plugin", "source": "pkgs/plugins/zellij-plugin", "component": "zellij-plugin", "current_version": "0.1.0", "stable_tag": "zellij-plugin-v0.1.0", "commit_distance": 7, "version": "0.1.1-next.7.1", "release_tag": "zellij-plugin-v0.1.1-next.7.1" }
The resolver MUST:
- resolve the exact Moon target;
- reject unknown or non-publishable targets;
- read Moon
sourceas package key and version-source location; - use the exact Moon project ID as component identity;
- read version from
package.json, otherwise Cargo metadata forCargo.toml; - reject absent or malformed semantic versions;
- reject source branches other than
mainorrelease/*; - generate
<moon-project-id>-v<version>.
Version rules:
textlatest: current source version next/main: next minor -> <next>-next.<commit-distance>.<run-attempt> next/release/*: next patch -> <next>-next.<commit-distance>.<run-attempt>
Commit distance is component-specific:
- Find the latest reachable
<moon-project-id>-v<version>stable tag for the component; exclude prerelease tags. - Count first-parent commits from that tag to
HEAD. - If none exists, count first-parent history from repository root.
- Append
github.run_attemptas the final prerelease identifier.
Do not use github.run_number as the primary prerelease identity. Source-derived commit distance keeps manual and repository dispatch metadata consistent for the same commit.
When migrating from old source-path tags, use the latest reachable <project-source>-v<version> stable tag only as a temporary baseline fallback if no Moon-ID component tag exists. New tags MUST use the Moon project ID. Remove fallback after each component has a component-named stable release.
Publish Workflow Security and Execution
Normalize trigger values before checkout. Then:
- Require non-empty target, source SHA, and source branch.
- Require channel exactly
nextorlatest. - For repository dispatch, require an explicit trusted coordinator actor/App allowlist.
- Fetch full history plus
mainandrelease/*refs. - Require source branch to be
mainorrelease/*. - Verify source SHA is reachable from the claimed source branch.
- Check out the exact source SHA before executing repository code with publish credentials.
- Set up Moon/Bun/Rust and install dependencies.
- Run the metadata resolver with
github.run_attempt. - Export
PUBLISH_TARGET,PUBLISH_CHANNEL,PUBLISH_VERSION,PUBLISH_RELEASE_TAG, andPUBLISH_REF. - For
next, create/find a prerelease targeted atPUBLISH_REF. - For
latest, require the Release Please-created stable release to exist. - Run the Moon publish task.
- Summarize target, channel, version, release tag, branch, SHA, trigger, and result.
Prerelease creation MUST tolerate concurrent creation: after a failed gh release create, retry gh release view and fail only if the release still does not exist.
Use cancel-in-progress: false. Prefer release-tag-scoped locking when workflow expression limits permit; otherwise retain target/channel locking. Do not cancel retries of the same publication.
Branch Ownership
Keep these helper concepts:
textget-commits RELEASE_BRANCH MAIN_BRANCH is-root-commit COMMIT RELEASE_BRANCH MAIN_BRANCH is-hotfix-commit COMMIT RELEASE_BRANCH MAIN_BRANCH is-hotfix-merge COMMIT MAIN_BRANCH
Rules:
- Release branch root belongs to the prior stable
mainrelease and publishes nothing onrelease/*. - Later release-branch commits use hotfix policy.
- Hotfix history merged back to
mainpublishes nothing again. - New main work uses normal policy.
- Release Please on
release/*receives the current branch explicitly as target branch. - Associated PR head branch is the primary merge/squash signal; fetched ancestry is fallback.
Never rely only on commit equality. Squash merges create new hashes.
GitHub Automation Project
Add .github as a Moon project so workflow scripts and Bats tests join the normal task graph:
yaml# .moon/workspace.yml projects: sources: github: ".github"
yaml# .github/moon.yml id: github language: bash layer: tool tasks: test: command: "bats tasks/*.bats" inputs: - "actions/**/*" - "tasks/**/*" - "workflows/**/*"
Include actionlint in this task or a separate github:lint task.
Minimum Validation
Tests MUST cover:
- Node and Cargo Release Please component discovery;
- manifest version preservation and unsupported source failure;
- target-only, sorted affected publish matrix;
- source-derived latest/main-next/release-next metadata;
- component stable-tag lookup, prerelease exclusion, root fallback, and retry attempt;
- unknown target, non-publishable target, unsupported branch, and malformed version failures;
- channel normalization and rejection of old
tagfields; - trusted dispatch actor and source reachability;
- exact SHA checkout;
- stable release existence and idempotent prerelease creation;
PUBLISH_RELEASE_TAGuse for GitHub assets;- release-root and hotfix merge-back skips.
Run:
bashmoon run github:test moon run deployment:test actionlint .github/workflows/release.yml .github/workflows/publish.yml bash -n .github/tasks/* bash -n pkgs/tools/deployment/* moon run zellij-plugin:test moon run zellij-plugin:check moon run pi-extension:typecheck git diff --check
Common Mistakes
| Mistake | Correction |
|---|---|
Dispatch version/release tag from release.yml | Dispatch target, channel, source SHA, and source branch only |
Treat publish.yml as a dumb executor | It validates source and computes metadata after immutable checkout |
Use PUBLISH_TAG for both channel and release tag | Use PUBLISH_CHANNEL and PUBLISH_RELEASE_TAG |
Put manual changed/all/project selection on release.yml | Keep manual target/channel publishing on publish.yml |
Return rich metadata from get-publish-matrix | Return sorted {target} entries only |
Use run_number prerelease versions | Use component first-parent distance plus run_attempt |
Require package.json for Release Please sync | Support Node and Cargo version sources |
| Use package path as human-facing component | Keep path as package key; set component to Moon project ID |
| Guess project-to-component mappings | Use Moon project ID directly |
Create stable releases in publish.yml | Require Release Please-created stable release |
Keep generate-publish-git-tag beside resolver | Migrate callers and remove old helper |
| Cancel concurrent/retry publication | Use cancel-in-progress: false and narrow locking |
Implementation Order
- Add tests for channel terminology and target-only matrix output.
- Update Release Please synchronization for publishable Node and Cargo projects.
- Synchronize configs/manifest and verify path keys plus Moon-ID component names.
- Reduce
get-publish-matrixto target selection. - Add
resolve-publish-metadata; migrate and remove the old helper. - Rename internal tag terminology to channel.
- Update release dispatch to send source identity without metadata.
- Harden publish validation, immutable checkout, metadata resolution, and release management.
- Update npm, docs, and GitHub asset consumers.
- Add/retain branch ownership gates and explicit hotfix target branch.
- Add the
.githubMoon project and run full validation. - Smoke-test manual next, normal stable, hotfix, and merge-back paths.
Portability Boundary
Copy across repositories:
- ownership boundaries;
- target/channel/source dispatch contract;
- metadata resolver behavior;
- branch ownership rules;
- source-path package keys plus stable project-ID component identity;
- validation and test cases.
Keep repository-specific:
- trusted dispatch actor/App allowlist;
- Release Please config filenames;
- publish credentials and environments;
- package-specific Moon publish commands;
- stable bump policy if a repository intentionally differs.

