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
12 changes: 12 additions & 0 deletions .github/scripts/check-all.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Every guard the CI runs, in one local command. The i18n checks need wp-cli and gettext.
set -uo pipefail
cd "$(dirname "$0")/../.."

fail=0
for check in check-versions check-no-network check-no-emdash check-js-translations check-pot-fresh check-mo-fresh; do
printf '\n== %s\n' "$check"
.github/scripts/"$check".sh || fail=1
done

exit "$fail"
22 changes: 22 additions & 0 deletions .github/scripts/check-js-translations.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# wp-cli 2.12 names make-json output after a truncated handle (assets/a.js),
# so WordPress never finds the file and JS strings silently stay in English.
set -uo pipefail

fail=0
expected_hash=$(printf '%s' 'assets/admin.js' | md5sum | cut -d' ' -f1)

for po in languages/biolinks-*.po; do
[ -e "$po" ] || continue
locale=$(basename "$po" .po); locale=${locale#biolinks-}
json="languages/biolinks-${locale}-${expected_hash}.json"
if [ -f "$json" ]; then
printf 'ok %s\n' "$json"
else
printf 'FAIL missing %s (wrong make-json hash?)\n' "$json" >&2
ls languages/biolinks-"${locale}"-*.json 2>/dev/null >&2 || true
fail=1
fi
done

exit "$fail"
27 changes: 27 additions & 0 deletions .github/scripts/check-mo-fresh.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# A stale MO silently serves the previous translations: the PO is never read at runtime.
set -uo pipefail

fail=0
translated() { grep -oE '[0-9]+ translated' | head -1 | grep -oE '[0-9]+'; }

for po in languages/biolinks-*.po; do
[ -e "$po" ] || continue
mo="${po%.po}.mo"
if [ ! -f "$mo" ]; then
printf 'FAIL %s has no compiled %s\n' "$po" "$mo" >&2
fail=1
continue
fi
po_count=$(msgfmt --statistics -o /dev/null "$po" 2>&1 | translated)
mo_count=$(msgunfmt "$mo" 2>/dev/null | msgfmt --statistics -o /dev/null - 2>&1 | translated)
if [ "${po_count:-x}" = "${mo_count:-y}" ]; then
printf 'ok %s and %s both carry %s translations\n' "$po" "$mo" "$po_count"
else
printf 'FAIL %s carries %s translations but %s carries %s, run wp i18n make-mo\n' \
"$po" "${po_count:-?}" "$mo" "${mo_count:-?}" >&2
fail=1
fi
done

exit "$fail"
13 changes: 13 additions & 0 deletions .github/scripts/check-no-emdash.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Em dashes are banned from user facing copy.
set -uo pipefail

hits=$(grep -rn $'—' readme.txt README.md languages includes templates assets/admin.js assets/front.js 2>/dev/null \
| grep -v 'assets/vendor' || true)

if [ -n "$hits" ]; then
printf 'FAIL em dash found in user facing copy:\n%s\n' "$hits" >&2
exit 1
fi

printf 'ok no em dash in user facing copy\n'
20 changes: 20 additions & 0 deletions .github/scripts/check-no-network.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# BioLinks ships zero external HTTP calls. That is a product promise, not a
# preference: nothing else in the codebase enforces it.
set -uo pipefail

paths=(biolinks.php uninstall.php includes templates assets)
prune=(-path './assets/vendor' -prune -o)

php_hits=$(grep -rnE "wp_remote_(get|post|head|request)|curl_init|curl_exec|fsockopen|file_get_contents\([[:space:]]*['\"]https?://" \
--include='*.php' "${paths[@]}" 2>/dev/null | grep -v 'assets/vendor' || true)

asset_hits=$(grep -rnE "src=['\"]https?://|@import[[:space:]]+url\(['\"]?https?://|fetch\(['\"]https?://|XMLHttpRequest" \
--include='*.php' --include='*.js' --include='*.css' "${paths[@]}" 2>/dev/null | grep -v 'assets/vendor' || true)

if [ -n "$php_hits$asset_hits" ]; then
printf 'FAIL external network access reintroduced:\n%s\n%s\n' "$php_hits" "$asset_hits" >&2
exit 1
fi

printf 'ok no external HTTP call outside assets/vendor\n'
30 changes: 30 additions & 0 deletions .github/scripts/check-pot-fresh.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# A string added without regenerating the POT never reaches translate.wordpress.org.
set -uo pipefail

command -v wp >/dev/null || { printf 'FAIL wp-cli is required\n' >&2; exit 1; }

tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

root_flag=()
[ "$(id -u)" -eq 0 ] && root_flag=(--allow-root)

if ! wp i18n make-pot . "$tmp/fresh.pot" \
--slug=biolinks --domain=biolinks --exclude=assets/vendor --package-name="BioLinks" \
"${root_flag[@]}" >"$tmp/log" 2>&1; then
printf 'FAIL wp i18n make-pot did not run:\n' >&2
cat "$tmp/log" >&2
exit 1
fi

msgids() { grep -E '^msgid ' "$1" | sort -u; }

if diff <(msgids languages/biolinks.pot) <(msgids "$tmp/fresh.pot") > "$tmp/diff"; then
printf 'ok languages/biolinks.pot covers every source string\n'
else
printf 'FAIL languages/biolinks.pot is stale, regenerate with wp i18n make-pot\n' >&2
printf ' < committed POT, > freshly generated\n' >&2
cat "$tmp/diff" >&2
exit 1
fi
45 changes: 45 additions & 0 deletions .github/scripts/check-versions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# The release version lives in four places and a mismatch on "Tested up to"
# makes the plugin drop out of the WordPress.org directory search.
set -uo pipefail

fail=0
err() { printf 'FAIL %s\n' "$*" >&2; fail=1; }
ok() { printf 'ok %s\n' "$*"; }

php_version=$(sed -n 's/^[[:space:]]*\*[[:space:]]*Version:[[:space:]]*\([0-9][0-9.]*\).*/\1/p' biolinks.php | head -1)
php_const=$(sed -n "s/.*BIOLINKS_VERSION'[^']*'\([0-9][0-9.]*\)'.*/\1/p" biolinks.php | head -1)
readme_tag=$(sed -n 's/^Stable tag:[[:space:]]*\([0-9][0-9.]*\).*/\1/p' readme.txt | head -1)
php_tested=$(sed -n 's/^[[:space:]]*\*[[:space:]]*Tested up to:[[:space:]]*\([0-9][0-9.]*\).*/\1/p' biolinks.php | head -1)
readme_tested=$(sed -n 's/^Tested up to:[[:space:]]*\([0-9][0-9.]*\).*/\1/p' readme.txt | head -1)

for pair in "biolinks.php Version:$php_version" \
"BIOLINKS_VERSION:$php_const" \
"readme.txt Stable tag:$readme_tag" \
"biolinks.php Tested up to:$php_tested" \
"readme.txt Tested up to:$readme_tested"; do
[ -n "${pair#*:}" ] || err "could not read ${pair%%:*}"
done
[ "$fail" -eq 0 ] || exit 1

if [ "$php_version" = "$php_const" ] && [ "$php_version" = "$readme_tag" ]; then
ok "version $php_version consistent across biolinks.php and readme.txt"
else
err "version mismatch: header=$php_version constant=$php_const stable tag=$readme_tag"
fi

if [ "$php_tested" = "$readme_tested" ]; then
ok "tested up to $php_tested consistent"
else
err "tested up to mismatch: biolinks.php=$php_tested readme.txt=$readme_tested"
fi

# readme.txt needs the version under both == Changelog == and == Upgrade Notice ==.
entries=$(grep -c "^= ${php_version} =$" readme.txt || true)
if [ "$entries" -ge 2 ]; then
ok "readme.txt documents $php_version in changelog and upgrade notice"
else
err "readme.txt has $entries entries for '= $php_version =', expected 2"
fi

exit "$fail"
104 changes: 104 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
name: CI

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

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

jobs:
guards:
name: Project invariants
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- name: Version is consistent across biolinks.php and readme.txt
run: .github/scripts/check-versions.sh
- name: No external HTTP call
run: .github/scripts/check-no-network.sh
- name: No em dash in user facing copy
run: .github/scripts/check-no-emdash.sh
- name: JS translation files carry the right handle hash
run: .github/scripts/check-js-translations.sh

lint:
name: PHP ${{ matrix.php }}
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
# 8.0 is the floor declared by "Requires PHP" in biolinks.php.
php: ['8.0', '8.1', '8.2', '8.3', '8.4']
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
- name: Lint every shipped PHP file
run: |
find biolinks.php uninstall.php includes templates -name '*.php' -print0 \
| xargs -0 -n1 -P4 php -l

i18n:
name: Translation freshness
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
coverage: none
- name: Install wp-cli and gettext
run: |
curl -sSLo "$RUNNER_TEMP/wp" https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x "$RUNNER_TEMP/wp"
sudo mv "$RUNNER_TEMP/wp" /usr/local/bin/wp
# gettext ships with the runner image. Only reach for apt if it does
# not, and never run apt-get update: unattended-upgrades holds the
# dpkg lock often enough to hang the job for hours.
if ! command -v msgfmt >/dev/null; then
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq gettext
fi
msgfmt --version | head -1
wp --version
- name: POT covers every source string
run: .github/scripts/check-pot-fresh.sh
- name: MO files match their PO
run: .github/scripts/check-mo-fresh.sh

plugin-check:
name: WordPress Plugin Check
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- name: Stage the plugin exactly as shipped
# Same exclusions as the release zip, so the check sees the real artifact
# and not the repo-only tooling. Keep in sync with CLAUDE.md.
run: |
mkdir -p build/biolinks
rsync -a --delete \
--exclude='.git/' --exclude='.github/' --exclude='build/' \
--exclude='README.md' --exclude='screenshots/' --exclude='tools/' \
--exclude='docs/' --exclude='CLAUDE.md' --exclude='Makefile' \
./ build/biolinks/
echo "::group::Staged files"
find build/biolinks -type f | sort
echo "::endgroup::"
- uses: WordPress/plugin-check-action@v1
with:
build-dir: './build/biolinks'
slug: biolinks
# The DirectDatabaseQuery and InterpolatedNotPrepared warnings are
# structural: the 5.9 floor rules out prepared %i placeholders.
ignore-warnings: true
Loading