Skip to content

wordpress.org readiness + CI/release pipeline - #1

Merged
lcswll merged 28 commits into
mainfrom
ci/wporg-pipeline
Oct 2, 2026
Merged

lcswll merged 28 commits into
mainfrom
ci/wporg-pipeline

Conversation

@lcswll

@lcswll lcswll commented Oct 1, 2026

Copy link
Copy Markdown
Owner

What

  • Plugin ready for wordpress.org: unique prefixes, %i identifiers in all SQL, full readme, directory assets (icon, banners, screenshots), translation fallback instead of load_plugin_textdomain().
  • Fixes found by the new tests: sender for API mailers (pre_wp_mail), remote-content notice no longer triggered by plain links.
  • CI (ci.yml): php -l 7.4–8.5 · PHPCS WordPress-Extra/VIP/PHPCompatibility · PHPStan level 8 · PHPUnit · Playground integration self-test + Playwright browser tests (PHP 7.4/WP 6.5 and PHP 8.4/latest) · official Plugin Check action + pinned Plugin Check PHPCS rules · ESLint no-unsanitized · gitleaks · composer/npm audit · actionlint + zizmor · reproducible, verified release ZIP.
  • Release (release.yml): tag vX.Y.Z → full CI → GitHub release with ZIP + SHA-256 → wordpress.org SVN deploy (off until WPORG_DEPLOY=true + SVN secrets).

Verified locally

npm run verify all green; e2e on PHP 8.3/WP 7.1 and PHP 7.4/WP 6.5 (45 integration assertions, 7 browser tests); Plugin Check 0 findings; actionlint/zizmor/gitleaks clean.

🤖 Generated with Claude Code

Plugin:
- Unique prefixes (outbox_mail_log / OutboxMailLog) as required by the plugin review
- All SQL identifiers via $wpdb->prepare() %i, direct-query use documented
- Bundled translation only as fallback to translate.wordpress.org packs
- Sender taken from the From header for API mailers (pre_wp_mail)
- Remote-content notice no longer triggered by plain links
- Full wordpress.org readme, directory icon/banners/screenshots

Pipeline (.github/workflows/ci.yml, release.yml):
- php -l 7.4-8.5, PHPCS WordPress-Extra + VIP + PHPCompatibility, PHPStan level 8
- PHPUnit + Brain Monkey, WordPress Playground integration self-test and
  Playwright browser tests on oldest and newest PHP/WP
- Official Plugin Check action + pinned Plugin Check PHPCS rules
- ESLint no-unsanitized, gitleaks, composer/npm audit, actionlint, zizmor
- Reproducible, verified release ZIP; tag release to GitHub and wordpress.org SVN

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@accesslint

accesslint Bot commented Oct 1, 2026

Copy link
Copy Markdown

AccessLint reviews pull requests for accessibility issues before they ship. Reviews on private repositories need a paid plan, but this pull request gets a review on us. Results will appear here shortly, and new pushes to this pull request will be re-reviewed so you can see fixes confirmed. Keep automated WCAG reviews running on this repo: https://app.accesslint.com/accounts/lcswll?source=payment_required

lcswll and others added 5 commits October 2, 2026 00:20
- "OUTBOX" is a registered software trademark (Outbox Technology CRB);
  guideline 17 forbids slugs starting with third-party trademarks.
  New name/slug: Mailspur – Email Log / mailspur-email-log, prefix mailspur.
- Plugin URI points to the public repository; repo-checks --online now
  verifies every URI in the header and readme (the old one was a 404).
- Admin tab navigation fully escaped (no bare echo of conditionals).
- debug_backtrace() documented as a feature (source detection).
- Readme: "What makes it different" section (guideline 18) and source link
  (guideline 4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Linux the Playground CLI can stay alive after run-blueprint finished, which
let the CI job run into its 30-minute limit without any output. The run now
ends as soon as selftest.php has written its report, has a hard timeout and
prints the Playground output in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- CI e2e ran into the 30-minute limit: Playground's php-wasm asyncify build
  crashes on Node 22 with PHP 7.4 ("RuntimeError: unreachable") and the CLI
  then hangs. Reproduced on Linux (WSL); green on Node 24. CI uses Node 24,
  e2e.mjs refuses older Node, job limit lowered to 15 minutes.
- Preview: remote src/srcset/background/poster, <link href>, CSS url() and
  @import are defused before rendering while remote content is blocked, so
  the browser never even attempts the request; the CSP stays as second layer.
  Linux Chromium had attempted the tracking-pixel request (blocked by CSP).
- Plugin Check action may post its PR comment (pull-requests: write, job only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

✅ WordPress Plugin Check Report

✅ Status: Passed

📊 Report

All checks passed! No errors or warnings found.


🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check

lcswll and others added 22 commits October 2, 2026 00:59
…inks stay errors

gnu.org dropped the request from the GitHub runner (fetch failed), which failed
the readiness job although the License URI is fine. Now: 3 attempts with a
15 s timeout; HTTP 4xx/5xx still fails, an unreachable host is a warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Read-only, resumable import of existing logs into Mailspur (Settings →
"Import from other plugins"), so a switch keeps the history:
WP Mail Logging, Email Log, Check & Log Email, FluentSMTP, Post SMTP,
SureMail, WP Mail Catcher, WP Mail Log.

- One Source class per plugin, mapped to the format each plugin actually
  writes (research of current code): site-local vs UTC vs "local epoch"
  timestamps, WP Mail Logging's literal ",\n" lists, esc_html'd subjects,
  serialized FluentSMTP/SureMail/Post SMTP columns, status/error semantics.
- Foreign serialized data is unserialized with allowed_classes=false
  (no PHP object injection), truncated data is handled.
- Batches of 250 via REST (admins only), per-source lock, stored progress:
  re-running only adds new rows, undo removes an import.
- Entries older than the retention period are skipped; secrets are
  redacted like for logged mails (shared Redactor).
- List sorted by send date (created_at) instead of id, retention/max-entries
  cleanup delete by date – imported old entries get new ids. New indexes
  (status, created_at) and (source); DB version 2.
- Tests: 10 mapping unit tests, Playground integration test with the real
  WP Mail Logging and Email Log plugins plus fixture tables for the others
  (42 assertions incl. time zones, no duplicates, read-only, undo,
  retention), browser test of the import UI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mails logged by two plugins in parallel, or already logged by Mailspur,
were imported again. Before each insert the importer now looks for an
entry from ANOTHER source with the same recipient set and subject sent
within ±120 s (indexed created_at window + LIKE pre-filter, comparison in
PHP that tolerates truncated / entity-escaped subjects and display names).
Mailspur's own entry always wins; rows of the same source are never merged.
Duplicates are counted and shown separately; window filterable via
mailspur_import_duplicate_window (0 disables).

Tests: 6 unit tests, integration test with the real WP Mail Logging and
Email Log logging the same mails Mailspur logs (three copies → one entry,
undo + re-import from the other plugin).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Module registry (src/Modules.php, Mailspur\Module) loaded first in boot.
- Logger: mailspur_meta filter in phases capture/phpmailer/result (+ import),
  mailspur_finalize_row before the single final UPDATE, mailspur_logged action;
  a failing module never loses the status update.
- Schema v3: meta (JSON), notes, size, raw columns; status HELD (3) for mails
  deliberately not delivered (staging).
- REST: mailspur_rest_summary / mailspur_rest_item payload filters, meta in detail.
- Admin: tab registry + mailspur_render_tab_{key}, mailspur_admin_enqueue,
  settings defaults/sanitize filters, settings sections and after-form actions.
- admin.js: window.mailspur API (tabs, row decorators, details, dialog actions,
  list listeners); list renders on DOMContentLoaded after module scripts.
- Translations per module in i18n/<locale>/*.json; e2e auto-discovers
  tests/e2e/features/*.php and tests/e2e/seed.d/*.php.
- Uninstall/deactivation clean every mailspur_* option, transient, cron hook
  and table generically.
- docs/MODULES.md documents the contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Static rule engine (mailspur_finalize_row, also for imports): HTML in
  text/plain, Gmail clipping, relative URLs, dev/staging hosts, unreplaced
  placeholders, mojibake, sender problems, bulk without List-Unsubscribe,
  recipient domain typos, link text/target mismatch, subject and alt checks.
  Codes + params in meta "notes", count in the notes column.
  Extensible via mailspur_note_rules / mailspur_note_texts.
- Dynamic checks when an entry is opened: MX/null MX per recipient domain
  (cached 1 day, filter mailspur_notes_mx_lookup) and repeated sending.
- Error explanations for PHPMailer, SMTP reply codes and API mailers.
- UI: severity badge in the list, error hint, Notes dialog tab, explanation
  with steps under the error; settings to disable and ignore codes.
- German translations, unit tests, Playground feature test, Playwright spec.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Sender check (SPF, DKIM, DMARC, MX) via dns_get_record() for the default sender and the most
  frequent From domains of the log, traffic lights + suggested records, cached 1 h (admin REST route).
- Staging mode: off / hold (log only) / redirect to test addresses; held entries get status held,
  redirected entries keep the original recipients in meta; admin bar item and Mail Log notices;
  suggestion on non-production sites (dismissible).
- Dialog actions "Send now" (release a held mail, bypassing staging once) and "Send to...".
- Rest::resend() accepts an optional validated "to" (array or comma list, max. 10, admins only).
- Unit, Playground integration (tests/e2e/features/delivery.php) and Playwright tests; German strings.
- ui.spec.js: wait for import.js's reload before navigating (race under load, net::ERR_ABORTED).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collects per mail (meta.trace): timeline (capture / phpmailer_init / result),
transport (mailer, SMTP host/port/encryption/auth, masked username; API
handler behind a pre_wp_mail short-circuit), origin (file:line, function,
hook stack), request context (type, method, path without query, user) and an
optional SMTP transcript with masked credentials and omitted message data.

"Download .eml" in the dialog: exact raw MIME source when the opt-in setting
is on (stored gzip+base64 in the raw column), otherwise a marked
reconstruction from the logged, redacted fields. Streamed as message/rfc822
through REST (rest_pre_serve_request).

Includes unit tests, a Playground integration test and a Playwright spec.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ation

- List filters for source (GET /sources with labels, cached), format (HTML/text),
  attachments and notes; part of the URL state, behind a 'More filters' toggle.
  Repository::filter() shares the WHERE clause between list, export and CLI.
- Streamed, keyset-paginated CSV/JSON export of the current filter (admin-post,
  nonce, view capability), formula-injection-safe CSV with BOM.
- wp mailspur list|show|resend|stats|purge|export|import (logic in Commands).
- Anonymise entries after N days on the daily cleanup (before deletion), masked
  recipients, marker in meta; badge and dialog note in the log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Integration fixes:
- Staging hold now runs first on pre_wp_mail (PHP_INT_MIN), so API mailers
  hooked there see the mail as handled and do not deliver on staging copies;
  covered by a new integration check.
- One shared WP_REST_Request/WP_REST_Response test stub (union of the two
  module copies), loaded by tests/bootstrap.php.
- Module list and generated translations merged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…idget, monitoring alerts

- Statistics tab: range presets + custom range, KPI tiles with deltas, stacked
  daily/weekly volume chart, failure-rate line, weekday x hour heatmap, top
  sources/domains/subjects linking into the filtered log. Hand-rolled SVG,
  keyboard + table views, no library.
- GET /stats: covering-index aggregation in UTC hours, converted to local days
  with wp_timezone() (DST-safe), bounded top-list sample, transient cache with
  generation-based invalidation.
- Dashboard widget (last 7 days, server-rendered SVG).
- Opt-in alerts: failure spike and silence (14-day baseline), email + webhook
  (Slack/Discord/JSON via wp_safe_remote_post), cooldown, recovery, history,
  test alert; WP-Cron every 15 minutes, cheap counter at send time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Core resend refuses anonymised entries (409) – the UI hid the button,
  the REST route did not; covered by the workflow integration test.
- Statistics: top-sender bars link to the log's new source filter.
- readme.txt: all module features, privacy note for opt-in webhooks,
  FAQ, six screenshot captions.
- Directory screenshots: log with note badges, preview, trace of a real
  web request (lost-password form), notes + explained error, statistics
  with 30 days of on-demand history (tests/e2e/screenshot-seed.php, never
  used by the specs), settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The value is a fixture: the test asserts it never appears in the SMTP
transcript. Documented in .gitleaksignore (history) and inline (future).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chromium on Linux logs "Blocked script execution in 'about:srcdoc'" when a
sandboxed preview contains scripts – that is the protection working. The
notes/trace specs already ignored these; delivery, insights and workflow now
do too (delivery failed on CI because of it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…output

- Dashboard widget chart is printed through wp_kses() with an explicit
  SVG/table allowlist instead of a "built from escaped parts" ignore.
- .eml download: documented why the body is not escaped (file download,
  message/rfc822 attachment) and send X-Content-Type-Options: nosniff.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lcswll
lcswll merged commit df423b1 into main Oct 2, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant