When I first started building the MoeKoe Music plugin ecosystem, there was no online plugin marketplace feature yet. Later, based on community suggestions, the plugin marketplace came into being [Add official/community plugin repositories to extend product functionality]

If plugins can be developed by the community, how should the plugin marketplace be managed?

The most straightforward approach would be to pull all plugin source code into a single monorepo. But the more I thought about it, the more awkward it felt. Each plugin has its own author, its own release cadence, its own build process. Stuffing them all into the official repo would not only drive up maintenance costs but also blur the lines of responsibility.

So this repository was ultimately not designed as a “plugin source code repository” but rather as a “plugin registration and indexing repository.” It only does a few things:

  • Accept plugin listing, update, delisting, and report submissions
  • Automatically run a round of basic validation and static identification
  • Record plugin snapshots when they pass manual review
  • Maintain a client-readable plugins.json
  • As a bonus, update the plugin list in the README for easy human reading

A traceable logbook: who submitted it, which version was reviewed, what the download URL is, whether it has network or file permissions — all captured in a clear set of data.

A Marketplace Index, Not a Source Repository

The most important file in the repository root is plugins.json. It is the actual data source consumed by the plugin marketplace.

{
  "id": "custom-app-background",
  "name": "Custom Background Image",
  "description": "Provides custom background image capability for MoeKoe Music, with transparency support.",
  "iconUrl": "...",
  "version": "1.0.0",
  "minversion": "1.6.1",
  "...":"....",
  "buildRequired": false,
  "networkAccess": false,
  "fileAccess": false,
  "binaryContent": false,
  "snapshot": {
    "iconUrl": "...",
    "repository": "MoeKoeMusic/custom-app-background-plugin",
    "commitSha": "dbf72d38c8cf6b1d1cefdf8ce15798d565678995",
    "downloadUrl": "hxx.zip",
    "release": null
  }
}

I didn’t want the marketplace record to always point to the “current latest code” of the plugin repository. Because the code an author commits today may not be the same as what was seen on the day of review. What the plugin marketplace should truly recognize is: the version that passed review.

So there is a core principle in this project:

What gets listed is not a drifting repository address, but a locked snapshot.

For plugins that don’t need compilation, the snapshot is pinned to the commit on the default branch at that time. For plugins that require building, the snapshot is pinned to the corresponding Release tag and its assets. This way, if problems arise later, you can look back and know exactly what was reviewed.

Actions Run First, Humans Make the Final Call

When a user submits an Issue, they need to select:

  • Whether it’s a “new listing” or “plugin update”
  • The GitHub repository address
  • Whether building is required before installation

The Action performs automatic validation, then writes the result back as a comment on the Issue, and labels the Issue with check-passed or check-failed.

Automation is responsible for organizing the facts; humans are responsible for making the final judgment:

  1. User submits a plugin using the Issue template
  2. Action parses the Issue form
  3. Validates repository, manifest, version, author permissions
  4. Locks the plugin snapshot
  5. Automatically comments with validation results
  6. Maintainer performs manual review
  7. Maintainer closes the Issue with Close as completed
  8. Another Action reads the validation result and generates a PR to update plugins.json and README.md
  9. After the PR is merged, the plugin officially enters the marketplace index

The “close method” is also leveraged here. If the Issue is closed as Close as not planned, the script will not execute the listing. In other words, GitHub’s native Issue status is used as the review button.

Snapshot Mechanism

If the user says “no building required before installation”:

  • Read the repository’s default branch
  • Get the current commit SHA
  • Use this commit to read manifest.json
  • Generate a source code zip URL pinned to that fixed commit
  • Download the zip, extract it, and perform a permission scan
  • Generate a repository-tree type snapshot

If the user says “building is required”:

  • Find the latest available Release
  • Take the first asset in the Release
  • Download the release asset
  • Extract the asset and read the manifest.json inside
  • Generate a release-asset type snapshot

These two paths solve the same problem: regardless of how the plugin source publishes, the end result must be a snapshot that is reviewable, downloadable, and traceable — one that won’t change just because the latest repository changes.

Permission Identification

Security review can’t rely solely on scripts making judgment calls. But scripts can help maintainers flag risk points first:

  • What permissions are declared in the manifest
  • Whether typical APIs or executable files appear in the source or release package

It doesn’t just say “risky” — it tells you why it determined the capability exists, for example:

  • Manifest declares network access
  • Source code uses fetch
  • Executable content like .exe, .dll, .node found
  • Source code uses localStorage or indexedDB

This is certainly not a perfect security scan. It may miss things or produce false positives. But its role is not to replace manual review — it’s to highlight the places maintainers should look at more closely.

It’s not a “judge”; it’s more like a “highlighter.”

Hidden Snapshot Data

After validation completes, a comment is posted on the Issue. In addition to human-readable validation results, the comment contains a hidden payload.

It generates an HTML comment like this:

<!-- plugin-publish-snapshot:base64... -->

This is a clever design. Because different GitHub Actions workflows don’t always share memory. Listing validation happens when the Issue is created or edited; actual listing happens when the Issue is closed. Hours or even days may pass between the two.

So how does the closing action know which snapshot originally passed validation?

The answer is: read it back from the Issue comment.

This turns the Issue comment into more than just a “notification” — it becomes a lightweight review record. It needs no extra database or service; it simply borrows GitHub’s own record-keeping ability.

Listing Action

The actual script that writes to plugins.json is scripts/publish-plugin-close.js.

It sets up several gates first:

  • The current event must be a publish-type Issue
  • The Issue must have state_reason === 'completed'
  • The user closing the Issue must be a repository maintainer
  • The validation comment’s payload must be check-passed
  • The payload must have a locked downloadUrl

These gates ensure one thing: ordinary users cannot write plugins into the marketplace by closing Issues or forging the process.

There’s a detail here: when updating a plugin, the original author is preserved — submitting an update Issue won’t change the author field. At the same time, updates must be submitted by the original author themselves, and the repository address must match the existing record.

New or updated plugins are placed at the top of the list. This choice also aligns with plugin marketplace intuition: the most recently reviewed content is shown first, making it easier for users and maintainers to see the latest changes.

Finally, a Markdown table is generated from the current plugins.json to replace the plugin list in the README.

Delisting and Reporting

The process is very similar to listing:

  1. User creates a delisting or report Issue
  2. plugin-moderation-validate.js checks whether the plugin exists
  3. If the author is voluntarily delisting, verify the submitter is the author
  4. Maintainer performs manual review
  5. Maintainer closes with Close as completed
  6. plugin-moderation-close.js changes the plugin status to delisted
  7. A PR is automatically generated to update plugins.json and the README

AI Review

The project also has a publish-plugin-ai-audit.js that performs an AI static audit on the plugin snapshot.

The script’s approach is:

  • Download the same snapshot
  • Select up to 24 candidate files
  • Prioritize manifest.json, package.json, entry files, and network/file/storage-related files
  • Send the content to an AI API
  • Request the AI to return structured JSON
  • Write the audit result back as a comment on the Issue

What I think works well here: the AI review is not designed as the “final judge.” It simply provides maintainers with an additional reference.

This is far more reliable than “AI says it’s safe so auto-list it.”

Why Not Use a Database or Backend Service?

The most interesting thing about this project isn’t how complex the code is, but rather how it avoids introducing extra systems.

It makes full use of what GitHub provides out of the box:

  • Issue Form as the application form
  • Issue comments as review feedback and snapshot records
  • Labels as auto-validation status
  • Close reason as manual review signal
  • Pull Request as the data change entry point
  • plugins.json as the marketplace index
  • README as the public display page
  • Git history as the audit log

So it doesn’t need a backend, doesn’t need a database, and doesn’t need a separate admin panel. For an open-source music player’s plugin marketplace, this level of complexity is just right.

ps: GitHub Actions is really great to work with, including my previous AJue’s Blog Internationalization Journey

Ending

Looking back at this project, it’s not a “heavy architecture” plugin platform. It’s more like a plainly dressed registration system where every step is traceable.

Users submit via Issues, Actions auto-check, maintainers manually confirm, and PR merges take effect. All key actions stay on GitHub, and all marketplace data lives in plugins.json. When there’s a problem, you can investigate; when there’s an update, there’s a record; when something is delisted, history isn’t erased.

It didn’t try to build a complete backend all at once — instead, it built the core trust chain that a plugin marketplace needs first.

The first real thing a community plugin ecosystem needs may not be a flashy interface, but a process that everyone can understand, trust, review, and continue to improve.

This repository MoeKoeMusic-Plugins came to be exactly this way.

Plugin Marketplace Web Version

Of course, none of the above was achieved overnight — it was built and refined step by step into what it is today.