Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# What a release carries.
#
# The distribution zip is `git archive` of the tag, so the `export-ignore`
# rules below are the only place that decides what is left out of it. `build/`
# is deliberately not ignored: the release tag commits the compiled assets, and
# a plugin without them renders no icon. `src/` is kept too, so the tagged tree
# can be rebuilt from itself.

/.github export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/CLAUDE.md export-ignore
/composer.lock export-ignore
/phpcs.xml.dist export-ignore
42 changes: 42 additions & 0 deletions .github/workflows/build-and-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Production build

# Keeps a `release` branch that is main plus the compiled `build/` directory,
# rebuilt on every push to main. It is there for sites that install the plugin
# from a branch rather than a tag, and it is not part of cutting a release: the
# Release workflow always builds fresh from main. The branch carries the
# `__VERSION__` placeholder, so it is not a versioned artifact.

on:
push:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true

jobs:
release-branch:
name: Update the release branch
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0

- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: '22'
cache: 'npm'

- name: Merge and build
uses: humanmade/hm-github-actions/.github/actions/build-to-release-branch@04c32a93e52ae987095f144105745a501d6207c8 # v0.2.0
with:
source_branch: main
release_branch: release
built_asset_paths: build
build_script: |
npm ci
npm run build
107 changes: 73 additions & 34 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,30 +1,56 @@
name: Release

# Builds the assets and stamps the version first, then creates the tag on top
# of the result, so a tag always points at code that is already built and
# already says its own version. The tag is written once and never moved, which
# is what Composer's VCS repositories and Packagist expect of a tag.
#
# Run this from the Actions tab and give it the version to cut.

on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Version to release, without a leading "v" (e.g. 1.2.3)'
required: true
type: string

concurrency:
group: release-${{ inputs.version }}
cancel-in-progress: false

jobs:
release:
name: Build and attach the plugin zip
name: Build, tag and publish
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Check the version input
env:
INPUT_VERSION: ${{ inputs.version }}
run: |
if ! printf '%s' "${INPUT_VERSION}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::Version must be X.Y.Z with no leading 'v'. Got: '${INPUT_VERSION}'"
exit 1
fi
echo "VERSION=${INPUT_VERSION}" >> "$GITHUB_ENV"
echo "TAG=v${INPUT_VERSION}" >> "$GITHUB_ENV"

- name: Checkout
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
ref: main
fetch-depth: 0

# The tag is what everyone installs, so it has to agree with the two
# places the version is written. `VERSION` is also the cache buster the
# stylesheets fall back to, so a stale one is not only cosmetic.
- name: Check the tag matches the plugin version
# Tags are immutable here, so fail before anything is built rather than
# after, and before a half-made release exists to clean up.
- name: Check the tag is free
run: |
VERSION="${GITHUB_REF_NAME#v}"
grep -q "^ \* Version: ${VERSION}$" button-block-icon.php \
|| { echo "Plugin header does not say ${VERSION}"; exit 1; }
grep -q "^const VERSION = '${VERSION}';$" button-block-icon.php \
|| { echo "VERSION constant does not say ${VERSION}"; exit 1; }
if git ls-remote --tags origin | grep -q "refs/tags/${TAG}$"; then
echo "::error::Tag ${TAG} already exists. Bump the version instead of moving it."
exit 1
fi

- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
Expand All @@ -35,33 +61,46 @@ jobs:
- name: Install Node dependencies
run: npm ci

# build/ is not committed, so the source archives GitHub generates carry
# no assets. This zip is the only artifact that installs and works.
- name: Build
run: npm run build

# main carries the literal `__VERSION__` in both places, so there is no
# version to disagree with the tag until this writes one.
- name: Stamp the version into the plugin
run: |
sed -i "s/__VERSION__/${VERSION}/g" button-block-icon.php
grep -n "Version:\|const VERSION" button-block-icon.php

- name: Create the release commit and tag
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'

# build/ is gitignored in the source tree, so force-add it: the whole
# point of the tag is that it is installable as it stands.
git add -f build
git add button-block-icon.php
git commit -m "Release ${TAG}"
git tag -a "${TAG}" -m "Release ${TAG}"

# Push the tag and the objects it reaches, and nothing else. main is
# left where it was.
git push origin "refs/tags/${TAG}"

# git archive honours the export-ignore rules in .gitattributes, so what
# ships is defined in one place and cannot drift from the tagged tree.
- name: Package
run: |
VERSION="${GITHUB_REF_NAME#v}"
mkdir -p dist/button-block-icon
cp button-block-icon.php README.md SECURITY.md LICENSE composer.json dist/button-block-icon/
cp -R inc build dist/button-block-icon/
cd dist && zip -rq "../button-block-icon-${VERSION}.zip" button-block-icon
mkdir -p dist
git archive --format=zip \
--prefix=button-block-icon/ \
-o "dist/button-block-icon-${VERSION}.zip" \
"${TAG}"

# A release cut by hand already exists, so upload into it rather than
# failing; a tag pushed on its own gets a draft to write notes in.
- name: Attach to the release
- name: Publish the release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${GITHUB_REF_NAME#v}"
ZIP="button-block-icon-${VERSION}.zip"

if gh release view "${GITHUB_REF_NAME}" > /dev/null 2>&1; then
gh release upload "${GITHUB_REF_NAME}" "${ZIP}" --clobber
else
gh release create "${GITHUB_REF_NAME}" "${ZIP}" \
--draft \
--title "${VERSION}" \
--generate-notes
fi
gh release create "${TAG}" "dist/button-block-icon-${VERSION}.zip" \
--title "${VERSION}" \
--generate-notes
16 changes: 10 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,21 @@ composer format # PHPCBF
```

The package is not on public Packagist. `README.md` documents the VCS
`repositories` entry a consuming site needs, and the fact that Composer installs
no built assets.
`repositories` entry a consuming site needs. A tagged version installs with its
built assets, since the tag commits `build/`; a `dev-main` install does not.

There are no automated tests, no test runner and no `wp-env` setup. CI
(`.github/workflows/ci.yml`) runs `lint:js`, `lint:css`, `build`, then
`composer lint`. Do not invent a test command.

Releases come from pushing a `v<version>` tag, which runs
`.github/workflows/release.yml`: it fails the tag unless both the plugin header
and the `VERSION` constant say the same version, then builds and attaches the
installable zip. Bump both strings in the same commit the tag points at.
Releases are cut by dispatching `.github/workflows/release.yml` from the
Actions tab with a version. It builds, stamps that version over the
`__VERSION__` placeholder in the plugin header and the `VERSION` constant,
commits the built `build/` with it, and tags that commit. Do not bump a version
by hand and do not push a `v` tag: `main` keeps the placeholder, the number
lives only in the tag, and a tag that already exists is refused rather than
moved. What the zip carries is set by `export-ignore` in `.gitattributes`, not
by the workflow.

Every enqueue in `inc/assets.php` is guarded on `is_readable`, so an unbuilt
checkout renders no icon and raises no error. Check `build/` exists before
Expand Down
49 changes: 37 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,13 @@ in `wp-content/plugins/button-block-icon` unless the root `composer.json`
overrides `installer-paths`. The site needs PHP 8.2 or later, and WordPress 7.1
or later for the Icons API.

Composer does not build the editor assets. A checkout installed this way still
needs the build below, either run in place or run in CI and shipped with the
deploy. Without it the plugin loads and renders no icon, since every enqueue is
guarded on the built file being there.
A tagged version carries its own built assets: the release workflow commits
`build/` into the tag before it is created, so `composer require` on a version
constraint gives you a plugin that works as installed. A `dev-main` install
does not, since `build/` is not committed on `main`. That one needs the build
below, either run in place or run in CI and shipped with the deploy; without it
the plugin loads and renders no icon, since every enqueue is guarded on the
built file being there.

Or clone it into `wp-content/plugins/` and run the build below.

Expand Down Expand Up @@ -277,11 +280,33 @@ CSS; `composer lint` runs PHPCS against the Human Made standard.

## Releases

Bump the version in the plugin header and in the `VERSION` constant, then push a
`v<version>` tag. `.github/workflows/release.yml` refuses a tag that disagrees
with either of them, builds the assets, and attaches
`button-block-icon-<version>.zip` to that tag's release, creating a draft if
there is not one already.

That zip is the artifact to install. The source archives GitHub generates carry
no `build/`, since it is not committed, and a plugin without it renders no icon.
Releases are cut by the **Release** workflow
(`.github/workflows/release.yml`), run by hand from the Actions tab with the
version to ship, written `1.2.3` with no leading `v`.

It builds the assets, writes that version over the `__VERSION__` placeholder in
`button-block-icon.php`, commits the built assets and the stamped file, and
tags *that* commit `v1.2.3`. Only the tag is pushed, so `main` stays where it
was. It then attaches `button-block-icon-1.2.3.zip` to the release.

Two things follow from building before tagging rather than after. A tag is
already built and already versioned, so it installs as it stands, whether
through Composer or as the zip. And the tag is written once and never moved,
which is what Packagist requires — the workflow refuses a version whose tag
already exists, so a bad release is superseded by the next patch version rather
than rewritten.

The version on `main` is always the literal `__VERSION__`, in both the plugin
header and the `VERSION` constant. The real number exists only inside a tag,
which is what keeps the two from ever disagreeing.

The zip is `git archive` of the tag, so `.gitattributes` is the single place
that decides what ships. `build/` and `src/` are both in it; CI config, the
PHPCS config and `composer.lock` are not. To change what a release carries,
edit `.gitattributes` — the workflow needs no change.

`.github/workflows/build-and-release.yml` separately keeps a `release` branch
in step with `main` plus a built `build/`, on every push to `main`. It is there
for installing the latest built code from a branch. It is not part of cutting a
release, and it carries the `__VERSION__` placeholder, so it is not a versioned
artifact.
4 changes: 2 additions & 2 deletions button-block-icon.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Plugin Name: Button Block Icon
* Plugin URI: https://github.com/humanmade/button-block-icon
* Description: Puts an icon beside the label on core/button, chosen from a registered icon collection or uploaded as a one-off SVG.
* Version: 1.1.2
* Version: __VERSION__
* Requires at least: 7.1
* Requires PHP: 8.2
* Author: Human Made Limited
Expand All @@ -20,7 +20,7 @@

namespace HM\Button_Icon;

const VERSION = '1.1.2';
const VERSION = '__VERSION__';

/**
* The plugin's own directory, with a trailing slash.
Expand Down
Loading