# Publish a repository listing from CI

> Keep a plugin's Maru listing current with a GitHub Actions job that tells Maru to read each new release, using an access token that can only publish listings.

Canonical: https://maru.audio/docs/publish-a-listing-from-ci
Documentation index: https://maru.audio/llms.txt

Add a short GitHub Actions workflow to a repository that carries `.maru/listing.json`, and each GitHub release you publish reaches your Maru page and Maru Desktop's updates within minutes. The job builds nothing and uploads nothing: it checks the file, then asks Maru to read the public repository and its new release.

This works for any repository with a listing, including one that holds only release files for a plugin built with another framework. [List your repository on Maru](https://maru.audio/docs/repository-listing.md) explains the file and what the page shows. The page labels your files as a community release that Maru hasn't reviewed or built, which people download at their own risk.

## Before you start

You need:

- `.maru/listing.json` committed to the repository, with `maru listing check` reporting no problems.
- A Maru account with the GitHub account that owns the repository linked at [Connections](https://maru.audio/account/connections). For a repository owned by a GitHub organization, the file's `credits` on the default branch must name your GitHub login with the role `owner` or `maintainer` (the copy at the job's `--ref` doesn't count: a ref can name a commit from a fork).

The listing doesn't need to exist yet. The first run creates it.

## Create a token that can only publish listings

Open [Access tokens](https://maru.audio/account/tokens) and create a token. Give it a name you'll recognise, such as `glass-hall CI`, clear the permissions that are selected for you, and select only `listings:publish` ("Publish and refresh repository listings you own"). Copy the token when it's shown; it isn't shown again.

With only that permission, the token can publish and refresh listings for repositories you own and nothing else. It can't create projects, spend credits or publish modules.

In your GitHub repository, open **Settings**, then **Secrets and variables**, then **Actions**, and add a repository secret named `MARU_TOKEN` with the token as its value. GitHub's own `GITHUB_TOKEN` can't replace it: it signs in to GitHub, not to Maru.

## Add the workflow

`maru listing init --ci` writes this file for you when it doesn't exist. Or save it yourself as `.github/workflows/maru-listing.yml`:

```yaml
name: Maru listing

on:
  release:
    types: [published]
  workflow_dispatch:

permissions:
  contents: read

jobs:
  publish-listing:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v7
        with:
          persist-credentials: false
      - uses: actions/setup-node@v7
        with:
          node-version: '24'
      - name: Validate .maru/listing.json
        run: npx --yes maruaudio@latest listing check
      - name: Tell Maru to read this release
        env:
          MARU_TOKEN: ${{ secrets.MARU_TOKEN }}
          MARU_LISTING_REF: ${{ github.event.release.tag_name }}
        run: npx --yes maruaudio@latest listing publish --ref "$MARU_LISTING_REF"
```

The first step checks out the repository so the CLI can read the file and find the repository from its `origin` remote. The validation step stops the job when the file has a problem, before anything reaches your page. The last step passes the release's tag with `--ref`, so Maru reads that release. The tag reaches the command through the `MARU_LISTING_REF` variable, quoted, so the shell never runs any part of it. A manual run has no tag, so the variable is empty and Maru reads the default branch.

Before you trust the workflow with your token, replace the version tags on Actions with the full commit SHAs you reviewed. The upstream
<a href="https://github.com/actions/checkout" target="_blank" rel="noopener noreferrer">checkout</a> and
<a href="https://github.com/actions/setup-node" target="_blank" rel="noopener noreferrer">setup-node</a>
references explain their inputs. To choose when the CLI changes, replace `maruaudio@latest` with an exact version.

Keep the workflow on releases and manual runs. Don't add `pull_request` or other triggers that let changes from people outside your project run with `MARU_TOKEN`.

## When it runs

The job runs when you publish a GitHub release, after the release and its files exist, so GitHub has already recorded each file's SHA-256 checksum. Maru reads those checksums; it never receives your files.

GitHub's `published` event includes prereleases. Maru records a prerelease as a beta, and your page shows betas only when the file's `plugin.releases.channel` is `all`. Drafts are never read.

You can also run the job from your repository's **Actions** tab (`workflow_dispatch`), for example after editing the file without making a release.

## Read the job log

The validation step prints what Maru would keep:

```text
plugins: glass-hall (effect)
display: README yes, media yes, mirror no

.maru/listing.json is valid.
```

The publish step prints each listing's page, such as `https://maru.audio/plugins/@your-login/glass-hall`, whether it is new, and the releases Maru read, with the newest tag. Problems in the file that Maru ignored are listed below; they don't fail the step, which is why the validation step runs first. Add `--json` to print the whole result for a script.

When the step fails, its exit code says why:

| Exit code | Meaning | What to check |
| --- | --- | --- |
| 3 | No token, or Maru rejected it | The `MARU_TOKEN` secret exists, the token isn't revoked and it has `listings:publish` |
| 1 | The file is missing or can't be read, so nothing was sent; or Maru refused the request | The message: run `maru listing check` locally for a file problem, or look for a GitHub account that isn't linked, a repository you don't own, missing `credits` for an organization repository, or too many requests |

Maru reads a repository at most once a minute, and an account can ask for ten reads an hour. A request over either limit is refused; run it again later. Running the job twice on an unchanged repository changes nothing.

## Run the same thing by hand

From a checkout of the repository, sign in once and ask Maru to read a release:

```bash
maru login
maru listing publish --ref v1.2.0
```

Add `--repo your-login/glass-hall-releases` when the checkout's `origin` remote points somewhere else. `maru listing status` prints your listings for the repository and when Maru last read them, without asking for a new read. The same read is available from [Repository listings](https://maru.audio/account/listings) with **Read again**.

## Revoke the token

Revoke the token from [Access tokens](https://maru.audio/account/tokens) when you stop using the workflow or think the secret was exposed. The listing stays as it is; the workflow then fails with exit code 3 until you save a new token as `MARU_TOKEN`.
