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.
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 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.jsoncommitted to the repository, withmaru listing checkreporting no problems.- A Maru account with the GitHub account that owns the repository linked at Connections. For a repository owned by a GitHub organization, the file's
creditson the default branch must name your GitHub login with the roleownerormaintainer(the copy at the job's--refdoesn'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 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:
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
checkout and
setup-node
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:
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:
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 with Read again.
Revoke the token
Revoke the token from Access 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.