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
push: 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:
PUBLISH_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:
{
"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:
workflow_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:
inputs.tag -> inputs.channel
client_payload.tag -> client_payload.channel
PUBLISH_TAG -> PUBLISH_CHANNEL
Do not rename native external CLI flags:
npm 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:
"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:
[
{ "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:
pkgs/tools/deployment/resolve-publish-metadata
Remove generate-publish-git-tag after migrating callers. Two helpers would duplicate version logic.
Interface:
resolve-publish-metadata <target> <channel> <source-branch> <run-attempt>
Return machine-readable metadata, preferably 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:
latest: 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:
get-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:
# .moon/workspace.yml
projects:
sources:
github: ".github"
# .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:
moon 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.