Document composite-action + submodule usage, config schemas, release ritual
This commit is contained in:
@@ -1,3 +1,204 @@
|
||||
# wp-release-ci
|
||||
|
||||
Generic release pipeline for a WordPress plugin or theme: validate, package, Gitea release, publish to a self-hosted update channel. Composite action + submodule.
|
||||
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: wordpress/wp-release-ci@v1 # PIN to a tag or SHA — never a moving branch
|
||||
with:
|
||||
channel_deploy_key: ${{ secrets.CHANNEL_DEPLOY_KEY }}
|
||||
```
|
||||
|
||||
> **Pin the ref.** Reference the action by a release tag (`@v1`) or a commit SHA, not by
|
||||
> `@main`. A composite action runs shell from its own checkout inside your repo's job; pinning
|
||||
> is what 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. Without it, the pipeline still cuts the Gitea
|
||||
release on the source repo but **skips** the channel publish.
|
||||
- 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.
|
||||
|
||||
Reference in New Issue
Block a user