Skip to content
Open
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
30 changes: 30 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Build output
dist/

# Distributable archives
builds/

# Strauss-prefixed runtime deps
dependencies/

# Node modules
node_modules/

# WordPress environment
.wp-env/

# Composer vendor
vendor/

# Test artifacts (minified vendor JS / traces — not lintable)
playwright-report/
test-results/

# tests/ holds Node lane-runner scripts (.mjs) + Playwright specs, not block
# source. lint:js is scoped to src/, so tests/ is never a release-gate target;
# ignoring it stops `lint-js --fix` from over-scanning and rewriting these files.
tests/

# Root build/tooling config — airo-wp-owned, not DSG block source. `lint-js
# --fix src/` otherwise silently reformats it on every dsg-sync.
webpack.config.js
53 changes: 53 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
module.exports = {
extends: ['plugin:@wordpress/eslint-plugin/recommended'],
rules: {
'import/no-extraneous-dependencies': 'off',
'import/no-unresolved': 'off',
'jsdoc/require-param-description': 'off',
// src/ is 100% DSG-owned and read-only (see CLAUDE.md): airo-wp never
// authors JS here, so lint must accept DSG's own conventions rather than
// error on source it cannot modify. The parent (DesignSetGo) lints this
// same source clean on an older, unpinned @wordpress/eslint-plugin where
// these rules were warnings; newer versions promote them to errors.
'@wordpress/no-unsafe-wp-apis': 'off',
'@wordpress/no-unused-vars-before-return': 'off',
'jsdoc/require-param-type': 'off',
'jsdoc/require-returns-description': 'off',
'jsdoc/check-line-alignment': 'off',
'no-nested-ternary': 'off',
'jsdoc/no-undefined-types': [
'error',
{
definedTypes: [
'JSX',
'Element',
'HTMLElement',
'HTMLImageElement',
'IntersectionObserver',
'NodeList',
'KeyboardEvent',
'Document',
],
},
],
},
overrides: [
{
files: ['tests/**/*.js', '**/*.test.js', '**/*.spec.js'],
env: {
jest: true,
},
rules: {
'no-unused-vars': [
'error',
{
varsIgnorePattern: '^_',
argsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
'jsx-a11y/label-has-associated-control': 'off',
},
},
],
};
74 changes: 74 additions & 0 deletions .github/scripts/stage-svn-payload.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
#
# Stages the distributable plugin zip as a directory tree for SVN, and verifies
# the staged tree matches the archive exactly.
#
# The zip is the canonical artifact: it is built from package.json "files" by
# tests/e2e/setup/build-zip.mjs, which is the same artifact the pre-release
# workflow runs Plugin Check and the e2e matrix against. Staging by unzipping
# that archive — rather than assembling a second file list — is what makes the
# bytes published to WordPress.org identical to the bytes CI tested.
#
# Usage: stage-svn-payload.sh <zip> <staging-dir>
#
# Writes payload=<dir> to $GITHUB_OUTPUT when set. Always prints a summary.

set -euo pipefail

ZIP="${1:?usage: stage-svn-payload.sh <zip> <staging-dir>}"
STAGING="${2:?usage: stage-svn-payload.sh <zip> <staging-dir>}"

if [ ! -f "$ZIP" ]; then
echo "::error::Archive not found: ${ZIP}" >&2
exit 1
fi

# The archive wraps everything in a single directory named after the plugin.
# Derive it rather than assume, so a change in archive layout fails loudly here
# instead of publishing a wrongly-nested tree to WordPress.org.
ROOTS=$(unzip -Z1 "$ZIP" | awk -F/ 'NF > 1 { print $1 }' | sort -u)
ROOT_COUNT=$(printf '%s\n' "$ROOTS" | grep -c . || true)

if [ "$ROOT_COUNT" -ne 1 ]; then
echo "::error::Expected exactly one top-level directory in ${ZIP}, found ${ROOT_COUNT}: $(printf '%s ' $ROOTS)" >&2
exit 1
fi

ROOT="$ROOTS"

rm -rf "$STAGING"
mkdir -p "$STAGING"
unzip -q "$ZIP" -d "$STAGING"

PAYLOAD="${STAGING}/${ROOT}"

if [ ! -d "$PAYLOAD" ]; then
echo "::error::Staging did not produce ${PAYLOAD}" >&2
exit 1
fi

# Compare the archive's file list against what landed on disk. unzip guarantees
# per-file byte fidelity, so the thing worth asserting is that the staged tree
# has neither gained nor lost entries.
archive_manifest=$(unzip -Z1 "$ZIP" | grep -v '/$' | sort)
staged_manifest=$(cd "$STAGING" && find . -type f | sed 's|^\./||' | sort)

if [ "$archive_manifest" != "$staged_manifest" ]; then
echo "::error::Staged tree does not match ${ZIP}" >&2
diff <(printf '%s\n' "$archive_manifest") <(printf '%s\n' "$staged_manifest") >&2 || true
exit 1
fi

file_count=$(printf '%s\n' "$staged_manifest" | wc -l | tr -d ' ')
byte_size=$(du -sk "$PAYLOAD" | awk '{print $1}')

echo "Payload staged from $(basename "$ZIP")"
echo " root: ${PAYLOAD}"
echo " files: ${file_count}"
echo " size: ${byte_size} KiB"
echo " top-level entries:"
( cd "$PAYLOAD" && ls -A1 | sed 's/^/ /' )

if [ -n "${GITHUB_OUTPUT:-}" ]; then
echo "payload=${PAYLOAD}" >> "$GITHUB_OUTPUT"
fi
106 changes: 106 additions & 0 deletions .github/scripts/svn-import-tag.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env bash
#
# Publishes a pre-release to WordPress.org as a tag only, leaving trunk alone.
#
# trunk/readme.txt's "Stable tag" is what WordPress.org feeds to the auto-update
# channel. A pre-release must not move it, so this never checks out or writes
# trunk: it imports the payload straight into tags/<version> in a single commit.
# That is why the stable lane's deploy action cannot be reused here — it always
# rsyncs into trunk first.
#
# Usage: svn-import-tag.sh <payload-dir> <svn-root> <slug> <version> <dry-run>
# dry-run: literal "true" or "false"
#
# Env: SVN_USERNAME, SVN_PASSWORD (required only when dry-run is false)

set -euo pipefail

PAYLOAD="${1:?usage: svn-import-tag.sh <payload-dir> <svn-root> <slug> <version> <dry-run>}"
SVN_ROOT="${2:?missing svn-root}"
SLUG="${3:?missing slug}"
VERSION="${4:?missing version}"
DRY_RUN="${5:?missing dry-run flag}"

TAG_URL="${SVN_ROOT}/${SLUG}/tags/${VERSION}"
TRUNK_URL="${SVN_ROOT}/${SLUG}/trunk"

if [ ! -d "$PAYLOAD" ]; then
echo "::error::Payload directory not found: ${PAYLOAD}" >&2
exit 1
fi

file_count=$(find "$PAYLOAD" -type f | wc -l | tr -d ' ')

if [ "$file_count" -eq 0 ]; then
echo "::error::Payload directory is empty: ${PAYLOAD}" >&2
exit 1
fi

if ! command -v svn >/dev/null 2>&1; then
echo "::error::svn is not installed. Subversion is not preinstalled on GitHub-hosted runners; install it before calling this script." >&2
exit 1
fi

# Fingerprint trunk so we can prove afterwards that it was untouched. Uses
# `svn cat` rather than an HTTP fetch so the same code path works against a
# file:// repository in tests. An absent readme is a legitimate state before the
# first stable release, and must fingerprint stably rather than error.
# sha256sum is coreutils (Linux runners); shasum is Perl (macOS). Pick whichever
# exists so this is runnable both on the runner and locally.
if command -v sha256sum >/dev/null 2>&1; then
sha256() { sha256sum | awk '{print $1}'; }
elif command -v shasum >/dev/null 2>&1; then
sha256() { shasum -a 256 | awk '{print $1}'; }
else
echo "::error::Neither sha256sum nor shasum is available; cannot fingerprint trunk." >&2
exit 1
fi

fingerprint_trunk() {
if out=$(svn cat "${TRUNK_URL}/readme.txt" --non-interactive 2>/dev/null); then
printf '%s' "$out" | sha256
else
printf 'absent'
fi
}

echo "Payload: ${PAYLOAD} (${file_count} files)"
echo "Tag URL: ${TAG_URL}"
echo "Trunk: ${TRUNK_URL} (never written by this lane)"

before=$(fingerprint_trunk)
echo "Trunk fingerprint before: ${before}"

if [ "$DRY_RUN" != "false" ]; then
echo "Dry run: no import performed. Would run:"
echo " svn import '${PAYLOAD}' '${TAG_URL}' -m 'Release ${VERSION} (pre-release)'"
exit 0
fi

: "${SVN_USERNAME:?SVN_USERNAME must be set for a real import}"
: "${SVN_PASSWORD:?SVN_PASSWORD must be set for a real import}"

echo "Importing ${file_count} files into ${TAG_URL}"

svn import "$PAYLOAD" "$TAG_URL" \
-m "Release ${VERSION} (pre-release)" \
--no-auth-cache \
--non-interactive \
--username "$SVN_USERNAME" \
--password "$SVN_PASSWORD"

# Post-conditions: the tag exists, and trunk is byte-identical to before.
if ! svn ls "$TAG_URL" --non-interactive >/dev/null 2>&1; then
echo "::error::Import reported success but ${TAG_URL} is not readable." >&2
exit 1
fi

after=$(fingerprint_trunk)
echo "Trunk fingerprint after: ${after}"

if [ "$before" != "$after" ]; then
echo "::error::trunk changed during a pre-release publish (${before} -> ${after}). A pre-release must never modify trunk, because trunk's Stable tag drives the auto-update channel." >&2
exit 1
fi

echo "Imported ${VERSION}; trunk unchanged."
Loading
Loading