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.

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 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 checkout, setup-node and upload-artifact 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.

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 release-event reference.

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 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 GitHub Actions billing. 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.