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
13 changes: 13 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Everything below exists for development only and must never reach the plugin
# zip. `bin/build-plugin-zip.sh` builds with `git archive`, which honours these,
# so this file is the single list of what is developer-only.
/.gitattributes export-ignore
/.github export-ignore
/.gitignore export-ignore
/.phpcs.xml.dist export-ignore
/README.md export-ignore
/bin export-ignore
/composer.json export-ignore
/docs export-ignore
/phpunit.xml.dist export-ignore
/tests export-ignore
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ jobs:
- name: WordPress coding standards
run: composer run phpcs

package:
name: Plugin zip
runs-on: ubuntu-latest
steps:
# The build reads .gitattributes and the version headers out of the
# commit itself, so it needs real history rather than a snapshot.
- uses: actions/checkout@v4
with:
fetch-depth: 0

# The same script the release workflow runs. It fails on a mismatched
# version, a stray development file or a wrong top-level directory, so
# packaging breaks in a pull request rather than at release time.
- name: Build the plugin zip
run: bin/build-plugin-zip.sh "$GITHUB_SHA"

test:
name: PHP ${{ matrix.php }} / WP ${{ matrix.wp }}
runs-on: ubuntu-latest
Expand Down
62 changes: 62 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Release

# Publishes citecue.zip — an archive that unpacks to citecue/ and contains only
# the runtime files — as a release asset, so that installing the plugin is
# "Plugins → Add New → Upload" rather than "download the source, rename the
# folder, delete the tests".

on:
push:
tags:
- 'v*'
workflow_dispatch:

jobs:
release:
name: Build and publish the plugin zip
runs-on: ubuntu-latest

permissions:
contents: write

steps:
# The build reads .gitattributes and the version strings out of the ref
# itself, so it needs the tag object, not just a shallow snapshot.
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Build the plugin zip
run: bin/build-plugin-zip.sh "$GITHUB_SHA"

# A tag that disagrees with the plugin header would ship an update
# WordPress cannot reason about; catch it before the asset is public.
- name: Check the tag matches the plugin version
if: startsWith(github.ref, 'refs/tags/v')
run: |
tag="${GITHUB_REF_NAME#v}"
header=$(sed -n 's/^[[:space:]]*\*[[:space:]]*Version:[[:space:]]*\([^[:space:]]*\).*/\1/p' citecue.php | head -n 1)
if [ "$tag" != "$header" ]; then
echo "::error::tag $GITHUB_REF_NAME does not match plugin version $header"
exit 1
fi

- name: Upload the zip as a workflow artifact
uses: actions/upload-artifact@v4
with:
name: citecue-plugin-zip
path: dist/citecue.zip
if-no-files-found: error

- name: Attach the zip to the release
if: startsWith(github.ref, 'refs/tags/v')
env:
GH_TOKEN: ${{ github.token }}
run: |
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
gh release upload "$GITHUB_REF_NAME" dist/citecue.zip --clobber
else
gh release create "$GITHUB_REF_NAME" dist/citecue.zip \
--title "$GITHUB_REF_NAME" \
--generate-notes
fi
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
*.zip
/dist/
.DS_Store
node_modules/
vendor/
Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ AI crawler (GPTBot, ClaudeBot, …) Human visitor

## Setup

1. Install and activate the plugin (upload this repo as a zip or drop it into `wp-content/plugins/`).
1. Download `citecue.zip` from the [latest release](https://github.com/citecue/wordpress-plugin/releases/latest), then install it under **Plugins → Add New → Upload Plugin** and activate it.
2. Open **Settings → CiteCue** and click **Connect to CiteCue**.
3. Confirm the project for this site in CiteCue. You are redirected back, and the plugin checks itself.

Expand Down Expand Up @@ -196,6 +196,18 @@ The worst case for an AI crawler is one 3 s wait per minute. For a human visitor

Plain PHP ≥ 7.4, no build step. Repo root is the plugin root, so the checkout can be symlinked straight into `wp-content/plugins/`.

### Releasing

GitHub's **Download ZIP** button is not an install path: it produces `wordpress-plugin-main.zip`, which unpacks to `wordpress-plugin-main/` and carries the tests and Composer files with it. WordPress keys a plugin by its directory name, so installs have to come from the release asset instead.

```bash
bin/build-plugin-zip.sh # writes dist/citecue.zip from HEAD
```

The script archives tracked files only, honouring the `export-ignore` rules in `.gitattributes`, so nothing untracked (a `vendor/`, a stray `.env`) can be swept in. It refuses to build unless `citecue.php`'s `Version:` header, `CITECUE_VERSION` and `readme.txt`'s `Stable tag:` all agree, and it checks the result unpacks to a single `citecue/` directory. CI runs the same script on every pull request.

To publish: bump those three version strings, then push a `vX.Y.Z` tag. The release workflow rebuilds the zip, fails if the tag disagrees with the plugin header, and attaches `citecue.zip` to the GitHub release.

### Tests

The suite is WordPress integration tests: real options, transients, REST requests and query conditionals, with the CiteCue API faked at the `wp_remote_get` layer (`tests/includes/class-citecue-http-mock.php`) so no test ever touches the network. WordPress core and its test library both come from Composer — there is nothing to download by hand.
Expand Down
89 changes: 89 additions & 0 deletions bin/build-plugin-zip.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
#
# Builds the installable WordPress plugin zip.
#
# GitHub's own "Download ZIP" button produces wordpress-plugin-main.zip, which
# unpacks to wordpress-plugin-main/ and carries the tests, CI config and
# Composer files with it. WordPress keys a plugin by its directory name — that
# name ends up in the plugins list, in update checks and in every support
# thread — so the distributed archive has to unpack to citecue/ and contain
# only the files the plugin loads at runtime.
#
# `git archive` gives both properties for free: --prefix sets the directory
# name, and it only ever reads tracked files honouring the export-ignore rules
# in .gitattributes, so an untracked vendor/, .env or editor backup can never
# be swept into a release.
#
# Usage: bin/build-plugin-zip.sh [ref] (ref defaults to HEAD)

set -euo pipefail

SLUG=citecue
REF=${1:-HEAD}

ROOT=$(git rev-parse --show-toplevel)
OUT_DIR="$ROOT/dist"
OUT="$OUT_DIR/$SLUG.zip"

if ! git rev-parse --verify --quiet "$REF^{commit}" >/dev/null; then
echo "error: '$REF' is not a commit in this repository" >&2
exit 1
fi

# Read the version out of the ref being built, not the working tree, so that
# building an old tag reports that tag's version.
show() { git show "$REF:$1"; }

header_version=$(show citecue.php | sed -n 's/^[[:space:]]*\*[[:space:]]*Version:[[:space:]]*\([^[:space:]]*\).*/\1/p' | head -n 1)
const_version=$(show citecue.php | sed -n "s/^[[:space:]]*define([[:space:]]*'CITECUE_VERSION'[[:space:]]*,[[:space:]]*'\([^']*\)'.*/\1/p" | head -n 1)
readme_version=$(show readme.txt | sed -n 's/^Stable tag:[[:space:]]*\([^[:space:]]*\).*/\1/p' | head -n 1)

if [ -z "$header_version" ]; then
echo "error: could not read the Version: header from citecue.php" >&2
exit 1
fi

# A plugin whose three version strings disagree updates incorrectly in the
# wild, and the mismatch is invisible until someone tries to upgrade.
if [ "$header_version" != "$const_version" ] || [ "$header_version" != "$readme_version" ]; then
echo "error: version mismatch in $REF" >&2
echo " citecue.php header: ${header_version:-<missing>}" >&2
echo " CITECUE_VERSION: ${const_version:-<missing>}" >&2
echo " readme.txt Stable tag: ${readme_version:-<missing>}" >&2
exit 1
fi

if [ "$REF" = HEAD ] && ! git diff --quiet HEAD -- .; then
echo "warning: the working tree has uncommitted changes; building HEAD as committed" >&2
fi

mkdir -p "$OUT_DIR"
rm -f "$OUT"
git archive --format=zip -9 --prefix="$SLUG/" --output="$OUT" "$REF"

# Guard the two properties the whole exercise is about: one top-level
# directory, correctly named, and no development files inside it.
entries=$(unzip -Z1 "$OUT")
stray_root=$(printf '%s\n' "$entries" | grep -v "^$SLUG/" || true)
if [ -n "$stray_root" ]; then
echo "error: archive has entries outside $SLUG/:" >&2
printf '%s\n' "$stray_root" >&2
exit 1
fi

stray_dev=$(printf '%s\n' "$entries" | grep -E "^$SLUG/(tests/|bin/|docs/|\.github/|composer\.|phpunit|\.phpcs|README\.md)" || true)
if [ -n "$stray_dev" ]; then
echo "error: development files leaked into the archive:" >&2
printf '%s\n' "$stray_dev" >&2
exit 1
fi

for required in "$SLUG/citecue.php" "$SLUG/readme.txt" "$SLUG/uninstall.php"; do
if ! printf '%s\n' "$entries" | grep -qx "$required"; then
echo "error: $required is missing from the archive" >&2
exit 1
fi
done

echo "Built $OUT (version $header_version, $(printf '%s\n' "$entries" | grep -cv '/$') files)"
printf '%s\n' "$entries" | sed 's/^/ /'
2 changes: 1 addition & 1 deletion readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Requires a CiteCue account.

== Installation ==

1. Upload the plugin to `/wp-content/plugins/` and activate it.
1. In WordPress, go to Plugins → Add New → Upload Plugin, choose `citecue.zip` and activate it.
2. Go to Settings → CiteCue and click "Connect to CiteCue".
3. Confirm the project for this site in CiteCue. You are redirected back and the plugin checks itself.
4. Add and generate optimized pages on CiteCue's Auto-Fix page.
Expand Down
Loading