209 lines
8.3 KiB
Markdown
209 lines
8.3 KiB
Markdown
# wp-release-ci
|
|
|
|
A generic, project-agnostic release pipeline for a **WordPress plugin _or_ theme** hosted on
|
|
Gitea. On a `vX.Y.Z` tag it validates the version, packages a distributable zip, cuts a Gitea
|
|
release on the source repo, and publishes the zip plus a computed update manifest to a
|
|
self-hosted **update channel** repo (the JSON a self-hosted updater polls).
|
|
|
|
It ships two pieces:
|
|
|
|
- **`release.sh`** — the pipeline. Runs from a checkout of your plugin/theme at the tagged commit.
|
|
- **`build-manifest.py`** — builds the published `${slug}.json` manifest from your curated
|
|
inputs (`.mu-manifest.yaml`, `readme.txt` sections, `CHANGELOG.md`).
|
|
|
|
It is packaged as a **Gitea Actions composite action** (`action.yml`), and can also be vendored
|
|
as a **git submodule**.
|
|
|
|
---
|
|
|
|
## Consuming it — composite action (recommended)
|
|
|
|
Add a thin release workflow to your plugin/theme repo. The action assumes you have **already
|
|
checked out the source repo with full history** (`fetch-depth: 0`).
|
|
|
|
`.gitea/workflows/release.yml`:
|
|
|
|
```yaml
|
|
name: Release on tag
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- 'v*'
|
|
|
|
jobs:
|
|
release:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0 # full history + the tag ref (git archive needs this)
|
|
|
|
- uses: https://gitea.mulligan.casa/wordpress/wp-release-ci@v1 # full Gitea URL; PIN the ref
|
|
with:
|
|
channel_deploy_key: ${{ secrets.CHANNEL_DEPLOY_KEY }}
|
|
```
|
|
|
|
> **Use the full Gitea URL.** On Gitea Actions a bare `owner/repo` in `uses:` resolves against
|
|
> **github.com**, not this instance — it must be the full `https://gitea.mulligan.casa/…@ref` form,
|
|
> or the runner tries to check the action out from GitHub and the job fails.
|
|
>
|
|
> **Pin the ref** to a release tag (`@v1`) or commit SHA, never `@main`. A composite action runs
|
|
> shell from its own checkout inside your repo's job; pinning stops an upstream change from
|
|
> silently altering what runs against your secrets.
|
|
|
|
If your project bundles a Vite app (`app_build: true`), also add a `actions/setup-node@v4` step
|
|
before the release step.
|
|
|
|
### Action inputs
|
|
|
|
| Input | Required | Default | Maps to env |
|
|
|----------------------|----------|----------------|---------------------|
|
|
| `channel_deploy_key` | yes | — | `CHANNEL_DEPLOY_KEY`|
|
|
| `channel_ssh_port` | no | `30009` | `CHANNEL_SSH_PORT` |
|
|
| `channel_branch` | no | `main` | `CHANNEL_BRANCH` |
|
|
| `channel_git_name` | no | `release bot` | `CHANNEL_GIT_NAME` |
|
|
|
|
`GITHUB_REPOSITORY`, `GITHUB_REF_NAME`, `GITHUB_SERVER_URL`, and `GITHUB_TOKEN` are provided by
|
|
the runner and consumed directly.
|
|
|
|
---
|
|
|
|
## Consuming it — git submodule (fallback)
|
|
|
|
Vendor this repo into your plugin/theme (submodules are directory-level — the whole repo lands
|
|
at one path, conventionally `.ci/`):
|
|
|
|
```bash
|
|
git submodule add https://gitea.mulligan.casa/wordpress/wp-release-ci .ci
|
|
git commit -m "Add wp-release-ci as .ci submodule"
|
|
```
|
|
|
|
Then check out submodules in your workflow and run `release.sh` directly:
|
|
|
|
```yaml
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
submodules: true # required — pulls the .ci/ submodule contents
|
|
|
|
- name: Release
|
|
env:
|
|
CHANNEL_DEPLOY_KEY: ${{ secrets.CHANNEL_DEPLOY_KEY }}
|
|
CHANNEL_SSH_PORT: ${{ vars.CHANNEL_SSH_PORT }}
|
|
CHANNEL_BRANCH: ${{ vars.CHANNEL_BRANCH }}
|
|
CHANNEL_GIT_NAME: ${{ vars.CHANNEL_GIT_NAME }}
|
|
run: bash .ci/release.sh
|
|
```
|
|
|
|
`release.sh` locates `build-manifest.py` next to itself, so this works from the submodule path
|
|
unchanged. To update the vendored copy later: `git submodule update --remote .ci`.
|
|
|
|
---
|
|
|
|
## `.mu-release.yaml` (in your plugin/theme repo root)
|
|
|
|
Per-project pipeline config. Read from the repo root; export-ignored from the shipped zip.
|
|
|
|
| Key | Required | Default | Purpose |
|
|
|-------------|----------|-----------------------|----------------------------------------------------------------------|
|
|
| `channel` | **yes** | — (hard-fails if unset) | Target channel repo `owner/name` the manifest + zip publish to. |
|
|
| `type` | no | `plugin` | `plugin` or `theme` — the master switch for validation & manifest. |
|
|
| `main_file` | no | per `type` (below) | Override the file the `Version:` header is read from. |
|
|
| `app_build` | no | `false` | Build `app/` (Vite) and gate on `assets/app` staleness before release.|
|
|
| `sections` | no | `{ autogen: true }` | How modal sections are sourced (see `build-manifest.py`). |
|
|
|
|
`main_file` defaults: `theme` ⟹ `style.css`, `plugin` ⟹ `${slug}.php` (slug = the repo name).
|
|
|
|
There is **no default `channel`** — this is deliberate. A missing `channel:` prints a
|
|
`::error::` and exits non-zero, so a misconfigured repo can never silently publish to the wrong
|
|
place.
|
|
|
|
**Theme example** (`aa/maineaa`, publishing to `aa/maineaa-updates`):
|
|
|
|
```yaml
|
|
channel: aa/maineaa-updates
|
|
type: theme
|
|
```
|
|
|
|
**Plugin example:**
|
|
|
|
```yaml
|
|
channel: aa/maineaa-updates
|
|
type: plugin # optional (default)
|
|
app_build: true # only if the plugin bundles a Vite app in assets/app
|
|
sections:
|
|
autogen: true # default modal sections from readme.txt == Section == blocks
|
|
strip_title: true # drop a leading H1 from parsed markdown
|
|
```
|
|
|
|
---
|
|
|
|
## `.mu-manifest.yaml` (optional, repo root)
|
|
|
|
Curated base fields merged into the published manifest. Computed/release-derived keys always
|
|
win (`version`, `slug`, `download_url`, `last_updated`, `icons`, `screenshot_url`, `requires`,
|
|
`tested`, `requires_php`); everything else here overrides. Section bodies default to
|
|
`readme.txt`'s `== Section ==` blocks unless you override them here.
|
|
|
|
```yaml
|
|
homepage: https://gitea.mulligan.casa/aa/maineaa
|
|
author: Mike Mulligan
|
|
author_homepage: https://gitea.mulligan.casa/mmulligan
|
|
tags:
|
|
- theme
|
|
- events
|
|
# Optional per-section overrides (inline HTML, or `@path.(md|html|txt)` includes):
|
|
# sections:
|
|
# description: "@docs/description.md"
|
|
```
|
|
|
|
**Icons vs screenshot.** A plugin's manifest gets an `icons` map built from
|
|
`icons/icon-128x128.png` + `icons/icon-256x256.png` when present. A theme's manifest gets a
|
|
single `screenshot_url` built from `screenshot.png` at the repo root when present.
|
|
|
|
---
|
|
|
|
## Required secrets / vars on the consuming repo
|
|
|
|
- **`CHANNEL_DEPLOY_KEY`** (secret) — an SSH private key whose **public** half is registered as a
|
|
**deploy key with WRITE access on the channel repo** (e.g. `aa/maineaa-updates`). This is what
|
|
lets the pipeline push the new zip + manifest. It is **required** — the publish step hard-fails
|
|
if it's unset (`channel:` is a required target, so reaching it is required too).
|
|
- Optionally `CHANNEL_SSH_PORT`, `CHANNEL_BRANCH`, `CHANNEL_GIT_NAME` (repo variables) if you
|
|
need non-default values.
|
|
|
|
---
|
|
|
|
## The release ritual
|
|
|
|
1. Bump the version **everywhere** it must agree with the tag:
|
|
- the `Version:` header — in `style.css` (theme) or your main plugin file (plugin);
|
|
- for **plugins**: the `Stable tag:` in `readme.txt`, and (if you keep one) the `*_VERSION`
|
|
PHP constant;
|
|
- add the release entry to your changelog (`readme.txt` `== Changelog ==` and/or `CHANGELOG.md`).
|
|
2. Commit.
|
|
3. Tag and push:
|
|
|
|
```bash
|
|
git tag v1.4.0
|
|
git push origin v1.4.0
|
|
```
|
|
|
|
The pipeline hard-fails if the header version, `Stable tag:` (plugins), and tag disagree. For
|
|
themes, the `readme.txt` `Stable tag:` / `License:` / `== Changelog ==` checks are skipped and
|
|
the version comes from `style.css`'s `Version:` header.
|
|
|
|
---
|
|
|
|
## What the pipeline does, in order
|
|
|
|
1. **Validate** the `Version:` header (and, for plugins, `readme.txt` `Stable tag:` / `License:`
|
|
/ `== Changelog ==`) all match the tag. A `*_VERSION` const is checked when present.
|
|
2. **Optional Vite build** (`app_build: true`) with a staleness gate on `assets/app`.
|
|
3. **Package-hygiene scan** — refuse to ship stray dev/scratch files.
|
|
4. **Package** `${slug}-${version}.zip` via `git archive` (honours `.gitattributes export-ignore`).
|
|
5. **Cut a Gitea release** on the source repo and attach the zip (idempotent on re-run/re-tag).
|
|
6. **Publish** the zip + computed `${slug}.json` manifest to the channel repo over SSH, with
|
|
retry + jitter for concurrent releases.
|