Updated

Releases and files

Versions and the files people download — uploads, size limits, scan status, and mirroring releases automatically from a repository you have proven is yours.

A release is a version of an item: a version string, optional notes, and a set of files. Mods, assets and servers can all cut them.

The release itself

versionstringrequired

The version string. Free text — we do not impose semver, because plenty of projects do not use it.

titlestring

A headline for the release (“Bug fixes”, “The Vehicles Update”).

descriptionstring

One line, for lists.

contentmarkdown

The full release notes.

hiddenbooleandefault false

A draft release. Behaves like a hidden item: invisible to everybody without access.

filesstring[]

File ids attached to this release.

Followers of the item can be notified when a release lands — it is on by default for anybody who follows something. See Follows.

Files

Files are uploaded separately and attached by id. They are always owned by the uploader and live in a per-user namespace.

Limit Standard account Supporter and above
Per file 20 MB 1 GB
Files per API request 20 20

The browser uploader and the API apply the same allowance. They differ in mechanism: the browser presigns a direct upload to storage, while the API takes the bytes itself, so no upload credential is ever handed out to a script.

Scan status

Every uploaded file carries a scan status — our scan, run on our side.

That is separate from virusScanLink on the item, which is an author-supplied link to a third-party report. The item page renders the two separately, so a reader can tell “the author says they scanned it” from “we scanned it”. Do not conflate them, and do not present one as the other.

Uploading through the API

Raw bytes — the cheapest thing to call from a shell script:

curl -X POST "https://api.moddingcommunity.com/api/content/file?name=mod.zip" \
  -H "Authorization: Bearer $TMC_TOKEN" \
  -H "Content-Type: application/zip" \
  --data-binary @dist/mod.zip

Or multipart, which accepts several files at once:

curl -X POST https://api.moddingcommunity.com/api/content/file \
  -H "Authorization: Bearer $TMC_TOKEN" \
  -F "file=@dist/mod.zip" \
  -F "file=@dist/readme.txt"

The two forms respond differently

Multipart always responds with an array, even for one file. Raw bytes responds with a single object. That is not an inconsistency to work around — it is what lets a caller know which form it used.

Attaching as you upload

Pass releaseId to attach the upload to a release directly:

curl -X POST "https://api.moddingcommunity.com/api/content/file?releaseId=42" \
  -H "Authorization: Bearer $TMC_TOKEN" -F "file=@dist/mod.zip"

A release belongs to a content item, so attaching to one is an edit of that item: you need write access to the parent mod, asset or server, not merely to have created the release. Access is checked before anything reaches storage.

The alternative — upload first, then reference the ids — is the better fit when you are replacing a release’s whole file set:

curl -X PUT https://api.moddingcommunity.com/api/content/asset/10/releases \
  -H "Authorization: Bearer $TMC_TOKEN" -H "Content-Type: application/json" \
  -d '[{ "id": 5, "version": "1.0.1", "files": ["<file-id>"] }]'

Mirroring releases from a repository

An author publishes v1.2.0 on their repository with notes, and the item’s Releases tab here says nothing until they remember to write it twice. A release source reads the first one and produces the second.

Available on mods and assets. An item has at most one release source.

Four switches, and they are not the same size

autodefault false

Import releases every night. This is the supporter perk, and the role is re-read against the configuring account on every pass rather than stamped when the row was written — a lapsed supporter’s source stops importing on its own. The manual pull in the editor is available to everybody who may edit the item.

prereleasesdefault false

Mirror prereleases as hidden releases rather than skipping them, so turning this on later does not silently leave a hole in your version history.

assetsdefault false

Also fetch the files the forge’s CI attached to each release. This is the switch that turns a notes mirror into an artifact pipeline: your own workflow builds the release, and we store what it produced rather than building it ourselves.

publishdefault false

Once those files have landed, publish the release as a signed pack. Assets only — a pack is addressed <owner>/<repo> and mounted into a running game, and a mod has no content id for anything to resolve. On a mod it is inert rather than an error.

There is deliberately no branch or ref setting. A release is named by its own tag; “the releases at a branch” is not a question any forge answers, and a field that looks like it narrows something and does not is worse than no field.

Proving the repository is yours

Attaching a repository needs write access on the item and nothing else — the same as every other feature that reads a repository URL. Mirroring release notes is the same: you can already write a release by hand, notification and all, so automating something you may already do grants nobody anything.

What needs a proof is the step after. Downloading a repository’s release artifacts and publishing them as a signed pack puts somebody’s files into players’ processes, under your name, signed by this site’s key. “I can paste a URL” is not a claim to that.

The proof is an identity match, not a token hunt

Unlike claiming a server, there is no token to place. Link your GitHub account on the Connections tab — the same sign-in flow — and we compare GitHub’s own durable user id against the repository’s owner. For an organisation we check its public members instead.

Nothing for you to do, no expiry window, and it survives a rename, because an id is not a name.

Six answers, each one actionable:

no-link No GitHub account is linked to your profile.
unsupported-host GitLab and Codeberg attach and mirror notes fine — but this site has an OAuth link for GitHub only, so there is nothing to match against for publishing.
not-yours The repository’s owner is not you.
org-private Your membership of that organisation is private. GitHub answers “not a member” and “cannot see” identically, so we report the one you can act on: make your membership public.
no-such-owner The owner does not exist.
unreachable We could not ask.

The proof is re-asked when it goes stale — a week by default — rather than on every pass, because it costs a request to GitHub. The host allow-list and the supporter role are local reads and are re-checked every tick.

An outside collaborator cannot be verified this way at all. That case is enumerated rather than hidden.

What the nightly pass will and will not do

A never-yet-read source imports its whole backlog silently

Pointing an existing project at its repository would otherwise fire a notification at every follower for every historical tag at once.

After that, at most ten releases per item per pass

A release fans out — every follower and every subscriber hears about it — so the rate is bounded even when a repository publishes a burst.

A version the item already has is never touched

Whoever wrote it. Mirrored releases carry the tag they came from, which is how a later pass can update its own work and leave a hand-written release alone — and how you can edit a mirrored release’s notes without the next pass reverting you.

A failure never removes a release

The item keeps the history it has. The reason for the last failure is shown on the source’s panel and cleared on success.

Removing the source does not delete your releases

They become ordinary hand-authored releases.

A repository whose top tag has not changed since the last pass costs nothing at all, which is what keeps a nightly job over a finished project free.