# Build and publish a module in CI

> Run Maru builds in GitHub Actions, retain the output and publish a trusted release to your Maru profile.

Canonical: https://maru.audio/docs/build-and-publish-in-ci
Documentation index: https://maru.audio/llms.txt

Run the same Maru commands in GitHub Actions that you use locally. Start with
a build you can download and inspect, then add publication when you create a
stable GitHub release. Your module source stays in its repository; Maru serves
the published version on your profile.

## Prepare a working build environment

This example is for a trusted repository and a runner where your module
already builds. Installing the public `maruaudio` npm package alone does not
currently provision all module-build components. The private native toolkit
also requires approved access; its current macOS ARM64 compiler-tools alpha
does not establish complete native plug-in exports or Windows/Linux support.
Arrange the matching build components before enabling this workflow.

Use a disposable self-hosted runner with your verified tools installed and add
the custom label `maru-build`. Keep it restricted to trusted repositories.
The example uses GitHub.com Actions and a macOS ARM64 runner, Node 24, an npm
project with a committed `package-lock.json`, and an exact `maruaudio` dependency
version. Use your project's package manager and lockfile if they differ.

From that environment, confirm the installed version provides these commands:

```sh
npx --no-install maru --version
npx --no-install maru doctor
npx --no-install maru module pack --help
npx --no-install maru publish --help
```

Resolve missing components before relying on CI. Do not download a different
CLI automatically to work around a version mismatch. The project should build
without a sibling checkout or dependencies that exist only on your laptop.
See [Check, build and export](https://maru.audio/docs/export.md) for what each command produces.

## Build directly in GitHub Actions

Save this as `.github/workflows/maru.yml`. It runs on manual invocation and
stable GitHub releases. It does not publish to Maru yet.

```yaml
name: Maru module

on:
  workflow_dispatch:
  release:
    types: [published]

permissions:
  contents: read

concurrency:
  group: maru-module-${{ github.repository }}
  cancel-in-progress: false

jobs:
  build:
    if: github.event_name != 'release' || !github.event.release.prerelease
    runs-on: [self-hosted, macOS, ARM64, maru-build]
    timeout-minutes: 30
    defaults:
      run:
        shell: bash
    steps:
      - name: Check out the requested commit
        uses: actions/checkout@v7
        with:
          ref: ${{ github.sha }}
          persist-credentials: false

      - name: Select Node
        uses: actions/setup-node@v7
        with:
          node-version: '24'
          package-manager-cache: false

      - name: Install locked dependencies
        run: npm ci

      - name: Check CLI and command availability
        run: |
          npx --no-install maru --version
          npx --no-install maru doctor
          npx --no-install maru module pack --help

      - name: Check the module and build a local Release
        run: |
          mkdir -p ci-reports
          npx --no-install maru module check . --fast --json > ci-reports/check.json
          npx --no-install maru module pack . --out ci-output --json > ci-reports/pack.json

      - name: Archive the built files
        run: tar -czf module-candidate.tar.gz -C ci-output .

      - name: Retain the build
        uses: actions/upload-artifact@v7
        with:
          name: module-${{ github.sha }}-${{ github.run_attempt }}
          path: module-candidate.tar.gz
          if-no-files-found: error
          retention-days: 7

      - name: Retain reports, including failed checks
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: reports-${{ github.sha }}-${{ github.run_attempt }}
          path: ci-reports/*.json
          if-no-files-found: warn
          retention-days: 7
```

Replace the version tags on Actions with the full reviewed commit SHAs before
enabling credentialed publication. The upstream
<a href="https://github.com/actions/checkout" target="_blank" rel="noopener noreferrer">checkout</a>,
<a href="https://github.com/actions/setup-node" target="_blank" rel="noopener noreferrer">setup-node</a> and
<a href="https://github.com/actions/upload-artifact" target="_blank" rel="noopener noreferrer">upload-artifact</a>
references explain their runner requirements and inputs.

The example assumes the module is at the repository root. For a monorepo,
set the run steps' `working-directory` to the module directory and adjust the
artifact paths, which are relative to the checkout root. Install dependencies
from the directory that owns the lockfile.

`--fast` is an early check, and the packed output is a build candidate. Run
your project's full checks before publication; passing this workflow does not
establish browser, native format, audio or DAW validation. Reports distinguish
checks that passed from those that were unavailable or skipped.

Open the workflow run's **Artifacts** section to download the candidate and
reports. Extract and test those exact files. The archive preserves file modes;
do not upload raw macOS plug-in bundles as loose files. Actions artifacts are
temporary build downloads, rather than a permanent customer release.

## Publish when a GitHub release is ready

First replace the module's provisional `local.` identity with your permanent
module ID. Prepare its listing and source license as described in
[Publishing](https://maru.audio/docs/publishing.md).

Create a GitHub environment named `maru-publish`, restrict its allowed release
refs, and add a secret named `MARU_TOKEN` containing a Maru token with
`modules:publish`. It authenticates to Maru; GitHub's own `GITHUB_TOKEN` cannot
replace it. Use an expiry and revoke the token when you stop using the workflow.

Add `environment: maru-publish` to the `build` job and add this step after your
full release checks, before report upload:

```yaml
- name: Run the full release checks
  if: github.event_name == 'release' && !github.event.release.prerelease
  run: |
    npx --no-install maru module check . --json > ci-reports/release-check.json

- name: Publish the trusted release to Maru
  if: github.event_name == 'release' && !github.event.release.prerelease
  env:
    MARU_TOKEN: ${{ secrets.MARU_TOKEN }}
  run: |
    npx --no-install maru publish . --source --json > ci-reports/publish.json
```

`maru publish` builds and uploads the project, then returns the module page and
its `@handle/slug@version` reference. It currently builds again rather than
uploading `ci-output` directly. `--source` includes the project source, private
by default. Making source public is a separate choice; do not add
`--source-visibility public` unless you intend to share it and have set its license.

The publication step evaluates module code while authenticated. Use reviewed
release source only. Keep this workflow off untrusted pull requests and do not
attach publication or compiler credentials to their checks. Protecting the
environment controls admission; it does not isolate the token from module code.

GitHub's `published` event includes prereleases, which is why the examples
filter them. Avoid also publishing on tag pushes: one GitHub release should have
one publication trigger. See the
<a href="https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#release" target="_blank" rel="noopener noreferrer">release-event reference</a>.

Maru assigns its publication version; a GitHub tag does not choose that number.
Check the existing release before retrying an interrupted upload on a fresh
runner. Local resume state is not automatically transferred between jobs, and
an upload that includes source should not be assumed to be an unchanged no-op.
Use `--expected-version` when you need to refuse replacing a newer publication.
Workflow concurrency reduces overlap, but does not order every queued GitHub
run or protect against a concurrent local or Studio publication.

## Add native targets separately

On a runner with a verified native toolkit, SDK and validators for the target,
use the same native build command as locally. For example, a VST3 build step is:

```yaml
- name: Build VST3 on a qualified runner
  run: |
    npx --no-install maru plugin build src/index.ts --release --format vst3 --out build/vst3
    npx --no-install maru module verify src/index.ts --build-root build
```

This step requires a working native export setup, beyond the compiler-tools
alpha. Expand to separate macOS, Windows and Linux jobs only as you can build
and test each target. Use `strategy.fail-fast: false`, enforce every required
target's result, and retain reports from failed jobs. AU uses a macOS runner.
Signing, notarization and installation checks remain separate steps.

Publish the web module once. Running `maru publish` in every native matrix row
does not attach those binaries to one website release. Website ingestion of
arbitrary CI-built native archives is not part of this example. Follow
[Package and release audio plug-ins](https://maru.audio/docs/packaging-and-releasing-plugins.md)
for archives, installers and native download delivery.

## Use GitHub-hosted runners or managed builds

The build commands are the same on a GitHub-hosted runner once its clean setup
can install the exact authorized components and pass the project's checks.
Changing `runs-on` alone does not supply the toolkit. Keep component provisioning
explicit and pinned before removing the preconfigured-runner requirement.

A workflow that compiles in your repository uses the repository owner's
GitHub allowance and billing. Calling a hosted build service instead runs its
compute in that service's account. See current limits and costs in
<a href="https://docs.github.com/en/billing/concepts/product-billing/github-actions" target="_blank" rel="noopener noreferrer">GitHub Actions billing</a>.
Connecting a GitHub account alone does not enable automatic Maru publication;
managed repository import and push-to-publish remain a separate integration.

For agent-assisted setup, read `maru guide read build-preview-export` and use
the installed CLI's `--help` before selecting build and publication options.
