Skip to content
Moku
@moku-labs/ci pages

Package publishing

package-ci.yml and package-release.yml: checks on every PR, a tokenless npm release on every merge.

On this page

Two workflows

WorkflowRuns onJobs
package-ci.ymlevery pull request and pushlint, types, test, build, extra, preview
package-release.ymla push to main, a dispatch, a published GitHub releasecheck → release → package → publish

setup writes both callers. Do not write the YAML by hand.

.github/workflows/publish.yml
name: Release

on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      release_type:
        type: choice
        required: true
        options: [patch, minor, major, prerelease]
  release:
    types: [published]

concurrency:
  group: publish-${{ github.ref }}
  cancel-in-progress: false

permissions:
  contents: read

jobs:
  release:
    uses: moku-labs/ci/.github/workflows/package-release.yml@v1
    permissions:
      contents: write
      id-token: write
    with:
      release_type: ${{ inputs.release_type }}
  1. A literal group, not github.workflow. The reused checks take Release-<ref>, and sharing it would deadlock.
  2. The ceiling for the whole pipeline. Inside, each job takes only the one it needs.

What each entry does

EventBumpThe release jobPublishes from
push to mainpatch, alwayscuts the tag and the GitHub releasethe new tag
workflow_dispatch on mainrelease_typecuts the tag and the GitHub releasethe new tag
release: publishednoneskippedthe release's tag

A version with a - goes to the npm dist-tag next, and its GitHub release is marked as a prerelease and never as latest. prerelease from a plain version produces an rc.

Inputs

All inputs are optional.

InputWorkflowDefaultMeaning
runs_onbothubuntu-latestRunner label for every job
bun_versionbothlatestPassed to setup-bun
validatebothtrueRun bun run validate after the build
extraboth[]JSON array of extra scripts, one job each
previewpackage-citruePublish each PR commit to pkg.pr.new
release_typepackage-releasenoneDispatch only. patch, minor, major or prerelease
publishpackage-releasetrueSet false to publish from a job of your own
node_versionpackage-release24Node for the publish job. It brings the npm that Trusted Publishing needs.
artifact_namepackage-releasenpm-packageName of the tarball artifact

package-release.yml outputs tag, version, prev_tag and artifact_name.

Project checks

A check the contract scripts do not cover goes in extra. Each script is its own job, after its own build, reported as ci / extra (check:bundle).

.github/workflows/ci.yml
jobs:
  ci:
    uses: moku-labs/ci/.github/workflows/package-ci.yml@v1
    with:
      extra: '["check:bundle", "test:cli-e2e"]'

Give publish.yml the same extra, so a release waits for the same checks. setup keeps a thin caller that has its own inputs. An extra job is not required until the ruleset names it.

PR previews

Every pull request commit is published to pkg.pr.new by the preview job. Nothing reaches npm and no token is in scope. A bot comments the install command on the PR.

try a PR in a consuming project
bun add https://pkg.pr.new/@moku-labs/core@42 # [!note] 42 is the PR number. A commit sha works too.
RuleWhere it holds
The pkg.pr.new GitHub App is installed on the repositoryonce per organization
The package repository is public. The consuming project may be private.pkg.pr.new
A preview URL never reaches mainthe lint job and the preview-deps check both refuse it
preview is not a required checkturn it off with with: { preview: false }

Dependabot

setup writes .github/dependabot.yml and .github/workflows/dependabot-automerge.yml.

  • Dependabot opens one grouped PR a day
  • the workflow turns on auto-merge
  • the required checks pass
  • the PR merges
  • the workflow dispatches a patch

A bump of devDependencies alone releases nothing. The published package is the same, and two packages that pin each other would release each other forever. Family packages skip Dependabot's 3-day cooldown: they are trusted the moment they are released.

A version only in tags

A package may keep its version in git tags and leave version out of package.json. build and preview then seed it from the latest v* tag before they pack. Nothing is committed.

When a release fails

start here
gh run view <id> --log-failed
bun run release:doctor
SymptomCauseFix
The run shows 0 jobs and never startsThe caller's concurrency group equals the child'sTake publish.yml back to the template
E422 on publishrepository.url differs from the repositorydoctor names it. Fix package.json.
The publish fails authNo trusted publisher, or the file was renameddoctor prints the npm trust github … line
wait says no publish.yml run startedpublish.yml has no push trigger, or GITHUB_TOKEN made the mergeAdd the trigger. For Dependabot, take the current dependabot-automerge.yml.
doctor warns that npm is behind the tagA run tagged but did not publishRe-run that release, or dispatch a patch
A new version ships old codeA dispatch releases origin/main, not your treeMerge first. The CLI refuses a HEAD that is not origin/main.

If npm ever rejects a publish made inside a reusable workflow of another repository, copy node_modules/@moku-labs/ci/examples/package/publish.local-publish.yml over publish.yml and keep the file name. It passes publish: false and runs npm publish in your own job. Check, release and package stay central. Do not switch before that happens.