diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7b53a62 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# Keep development-only paths out of the Composer dist tarball. +/tests export-ignore +/website export-ignore +/docs export-ignore +/.github export-ignore +/phpunit.xml export-ignore +/Makefile export-ignore +/renovate.json export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/.github/workflows/lint-pr-title.yml b/.github/workflows/lint-pr-title.yml new file mode 100644 index 0000000..8070846 --- /dev/null +++ b/.github/workflows/lint-pr-title.yml @@ -0,0 +1,19 @@ +name: Lint PR title + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + pull-requests: read + +jobs: + title: + runs-on: ubuntu-latest + steps: + # Enforce Conventional Commits on the PR title. Because PRs are + # squash-merged, the title becomes the commit subject on main, which + # release-please parses to decide the version bump and changelog entry. + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..d0840e2 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,24 @@ +name: Release + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + # Maintains a rolling release PR: parses Conventional Commit messages from + # squash-merged PRs, updates CHANGELOG.md and the plugin.php version header, + # and on merge creates the git tag + GitHub Release. Packagist picks up the + # new tag via the existing GitHub webhook. + - uses: googleapis/release-please-action@v4 + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..40ff6fe --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.2" +} diff --git a/composer.json b/composer.json index cf835c7..da717a7 100644 --- a/composer.json +++ b/composer.json @@ -13,8 +13,12 @@ "tangible/framework": "dev-main" }, "require": { + "php": ">=8.1", "symfony/yaml": "^6.4 || ^7.0" }, + "suggest": { + "tangible/framework": "Optional. Provides settings-page styling and framework integration; normally already loaded by other Tangible plugins in the WordPress environment." + }, "autoload": { "psr-4": { "Tangible\\": "./src" diff --git a/composer.lock b/composer.lock index f2ed2ae..0984ece 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "98099f8a30807bfbe8b3ee05e7c4056f", + "content-hash": "5e8c4d458f4a098ee175c0c8d76a0878", "packages": [ { "name": "symfony/deprecation-contracts", @@ -2116,10 +2116,12 @@ }, "prefer-stable": true, "prefer-lowest": false, - "platform": {}, + "platform": { + "php": ">=8.1" + }, "platform-dev": {}, "platform-overrides": { "php": "8.1.0" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } diff --git a/plugin.php b/plugin.php index c71c397..02c75b6 100644 --- a/plugin.php +++ b/plugin.php @@ -2,7 +2,9 @@ /** * Plugin Name: Tangible Object * Description: A WordPress tool suite for building data-driven interfaces with a clean extensible architecture. + * x-release-please-start-version * Version: 0.0.2 + * x-release-please-end-version * GitHub URI: TangibleInc/object */ diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..9a312a5 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "packages": { + ".": { + "release-type": "simple", + "package-name": "tangible/object", + "changelog-path": "CHANGELOG.md", + "include-component-in-tag": false, + "extra-files": [ + "plugin.php" + ] + } + } +} diff --git a/website/docs/development/releases.md b/website/docs/development/releases.md new file mode 100644 index 0000000..e1b3e14 --- /dev/null +++ b/website/docs/development/releases.md @@ -0,0 +1,132 @@ +--- +sidebar_position: 1 +title: Releasing +description: How versions are cut and published to Packagist +--- + +# Releasing + +Releases are automated. Merging pull requests into `main` is all a maintainer +normally does — versioning, the changelog, the git tag, the GitHub Release, and +the Packagist update all happen from there. + +## How it works + +We use [release-please](https://github.com/googleapis/release-please) with +[Conventional Commits](https://www.conventionalcommits.org/). The pipeline is: + +1. PRs are **squash-merged** into `main`. The PR title becomes the commit + subject on `main`. +2. release-please reads those commit subjects, works out the next version, and + maintains a rolling **release pull request** that updates `CHANGELOG.md` and + the `Version:` header in `plugin.php`. +3. Merging that release PR creates the git tag and the GitHub Release. +4. Packagist picks up the new tag automatically via the GitHub webhook and + publishes the version. + +Because the version is derived from the git tag, `composer.json` intentionally +has **no `version` field** — do not add one. + +## PR titles must follow Conventional Commits + +This is the one rule contributors need to follow. Since PRs are squash-merged, +the **PR title** is what release-please parses, and a non-conforming title +contributes nothing to the changelog or the version bump. PR titles are linted +in CI, so a bad title blocks the merge. + +The format is: + +``` +[optional scope]: +``` + +Common types and their effect on the version (once the package is `>= 1.0.0`): + +| Type | Example | Version bump | +|---|---|---| +| `fix` | `fix: guard against missing field type` | patch (`1.0.0` → `1.0.1`) | +| `feat` | `feat: add repeater field type` | minor (`1.0.0` → `1.1.0`) | +| `feat!` / `BREAKING CHANGE:` footer | `feat!: rename DataView storage API` | major (`1.0.0` → `2.0.0`) | +| `docs`, `chore`, `refactor`, `test`, `ci`, `build`, `perf`, `style` | `docs: expand field-type examples` | no release on its own | + +:::note Pre-1.0 behaviour +While the package is still on `0.x`, release-please never bumps to `1.0.0` on +its own — a breaking change bumps the **minor** instead (`0.1.0` → `0.2.0`). The +`1.0.0` release is cut deliberately. +::: + +### Breaking changes + +Signal a breaking change either by appending `!` after the type +(`feat!: ...`) or by adding a footer to the commit body: + +``` +feat: rename DataView storage API + +BREAKING CHANGE: `storage` now expects an adapter instance instead of a string. +``` + +With squash merges the PR body becomes the commit body, so a stray +`BREAKING CHANGE:` line in a PR description will also trigger a major bump — +keep descriptions clean and put the intent in the title. + +## Cutting a specific version (e.g. the first `1.0.0`) + +Normally you don't pick the version — release-please computes it from the +commits. To force a specific version, add a `Release-As:` footer to the commit +that lands on `main`. + +Two things to know about our setup: + +- `Release-As` is read from the commit **body**, not the subject. Appending it + to the PR title does **not** work (and would break the Conventional Commit + title and fail the lint). +- We squash-merge with **PR title only**, so the PR *description* does not become + the commit body either. You add the footer at merge time instead. + +The steps: + +1. Open a trivial PR — for example titled `chore: release 1.0.0`. +2. When squash-merging, **edit the commit message in GitHub's merge dialog** and + add the footer to the body (the "PR title only" default only prefills the + message — the description box is still editable): + + ``` + chore: release 1.0.0 + + Release-As: 1.0.0 + ``` + +3. release-please opens or updates its release PR pinned to `1.0.0`. Merge that + to tag `v1.0.0`. + +A maintainer who can push directly to `main` can do the same with an empty +commit: + +```bash +git commit --allow-empty -m "chore: release 1.0.0" -m "Release-As: 1.0.0" +git push origin main +``` + +:::warning +`Release-As` is one-shot — it only affects the release triggered by that commit, +so don't leave it in later merges. And do **not** hand-edit +`.release-please-manifest.json` to force a version: it records the *last +released* version, so setting it to `1.0.0` makes release-please think `1.0.0` +already shipped and it won't cut the release. +::: + +## Version constraints for consumers + +Once published, downstream projects install a released version rather than +`dev-main`: + +```bash +composer require tangible/object:^1.0 +``` + +- `^1.0` — receives all backwards-compatible minors and patches below `2.0.0` + (the conventional default). +- `~1.4.0` — patches only (`1.4.x`), no minor upgrades. +- Consumers are never upgraded silently: `composer.lock` pins the exact + installed version until they run `composer update`. diff --git a/website/sidebars.js b/website/sidebars.js index 673282c..b82cf1e 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -62,6 +62,13 @@ const sidebars = { 'examples/invoice-manager', ], }, + { + type: 'category', + label: 'Development', + items: [ + 'development/releases', + ], + }, 'api-reference', ], };