# Packaging and releasing

## The `.zip` format

A zip. `manifest.json` at the root, the source tree beside it, and an optional
`signature.json`. Nothing else — a zip is inspectable with tools people already
have, which matters when somebody is deciding whether to trust what is inside.

```
topic_stars.zip
├── manifest.json
├── signature.json        (optional)
├── TopicStars.php        the provider, named after the key
├── Controllers/
├── Templates/
└── Migrations/
```

The manifest and the provider must both be at the **root**. Zipping the folder
rather than its contents is the usual mistake, and it is refused with a message
saying so.

## The manifest

| Field | Required | Notes |
|---|---|---|
| `key` | yes | Lowercase letters, digits and underscores, starting with a letter. This is the directory name, the autoload segment and the migrator's module key, so it is checked strictly. |
| `name` | yes | Shown to administrators. |
| `version` | yes | Semantic version, `MAJOR.MINOR.PATCH`. |
| `description` | | One line, shown in the admin list. |
| `author` | | |
| `author_url` | | `http`/`https` only; anything else is dropped rather than rendered. |
| `convoro` | | Core version constraint, e.g. `^1.0`. |
| `requires` | | Other modules or extensions. |
| `provider` | | Provider class. Defaults to `Convoro\Extensions\<Key>\<Key>`, and must be under `Convoro\Extensions\`. |

`requires` takes either form:

```json
"requires": ["forum", "system"]
"requires": { "forum": "^1.0", "system": "^1.0" }
```

## Version constraints

`*`, an exact version, `^`, `~`, and `>=` `>` `<=` `<` `=`. Several separated
by a space or a comma all have to hold, e.g. `>=1.2 <2.0`.

`^1.2.3` allows anything below `2.0.0`. Below 1.0.0 the leftmost non-zero part
is what must not move, so `^0.2.3` stops at `0.3.0`. `~1.2.3` allows patch
moves only; `~1.2` allows the minor to move.

This is not Composer and does not try to be. There is no solver and no lock
file, because extensions install one at a time by hand and a solver would have
nothing to solve. A missing requirement is a sentence telling you what to
install.

## Building

```
php tools/convoro ext:package examples/topic_stars
```

The source directory must be named after the manifest key, because that is the
directory it installs into. Version control, editor files and `node_modules/`
are excluded. Without `--out`, the file is named `<key>-<version>.zip` beside
the source.

## Signing

Make a key pair once:

```bash
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out convoro-signing.pem
openssl rsa -in convoro-signing.pem -pubout -out convoro-signing.pub
```

Sign at build time:

```
php tools/convoro ext:package examples/topic_stars --key=convoro-signing.pem
```

Publish `convoro-signing.pub` so sites can add it to `trusted_keys`. Keep the
private key off the web server. Read [security.md](security.md) for what a
signature does and does not mean.

## Installing

```
php tools/convoro ext:install topic_stars-1.0.0.zip
```

Or upload the `.zip` from **Admin → Extensions**. Both run the same code and
the same checks; the admin route is behind `RequireAdmin`.

The upload limit shown in the admin panel is the smallest of Convoro's
`extensions.max_upload_size` (20 MB by default) and PHP's own
`upload_max_filesize` and `post_max_size`. If it reads lower than you expect,
PHP is the one deciding — and note that several PHP installations load no
php.ini at all and fall back to a 2 MB compiled-in default. Locally,
`tools/serve` sets them; on a server they belong in `php.ini`.

A package larger than `post_max_size` is discarded by PHP before Convoro sees
it, token and all. That case is detected and reported as a size problem rather
than the CSRF failure it would otherwise look like.

## Releasing a new version

**Bump `version` in `manifest.json` first.** Installing over the same version
is refused, not silently ignored — you are told to bump it or pass `--force`.

An upgrade replaces the directory rather than merging into it. Files the new
version dropped are gone: the package is the source of truth, so anything that
must survive belongs in the database or in `content/`. The staging directory is
swapped in only once it is complete, and the old one is restored if the swap
fails, so an interrupted upgrade leaves the site with the version it had.

**An upgrade never re-enables an extension the administrator had switched off.**

## Migrations

`Migrations/` runs forwards on install and on enable, and in reverse on an
uninstall that was asked to remove the data. Name your tables after your
extension; prefixes are applied for you.

```
php tools/convoro ext:uninstall topic_stars              # keeps the data
php tools/convoro ext:uninstall topic_stars --drop-data  # runs down(), asks first
```

Keeping the data is the default because `down()` dropping a table is the one
irreversible step in the system, and somebody uninstalling to try a different
build should not lose their records to a default they did not read.

Write `down()` properly. It is the only thing standing between an uninstall and
a table nobody can account for a year later.

## Checklist before you publish

- [ ] `version` bumped
- [ ] `convoro` constraint matches what you actually tested against
- [ ] `requires` lists everything you use from another module
- [ ] `down()` written and actually run once
- [ ] Everything from the database escaped before it reaches a hook's output
- [ ] CSRF checked on every state-changing route
- [ ] README says what it does, and what it talks to over the network
