FOSSE is a WordPress plugin bringing Social Web (ActivityPub-adjacent) features to WordPress sites. This repository is a single standalone plugin, not a monorepo.
- Repository:
Automattic/fosse - Main branch:
trunk - Plugin header:
fosse.php
| Component | Version / Tool |
|---|---|
| PHP | >=8.2 |
| WordPress | >=6.9 |
| PHPUnit | ^9.6 || ^11.0 (polyfills via yoast/phpunit-polyfills) |
| Test harness | automattic/wordbless (dbless engine; no MySQL needed) |
| PHP Coding Standards | automattic/jetpack-codesniffer (ruleset: Jetpack) — installed in tools/, runs on PHP 8.4 |
| JS Coding Standards | @wordpress/eslint-plugin + @wordpress/prettier-config |
| JS Tests | Jest (jsdom) |
| E2E | Playwright against WordPress Playground (@wp-playground/cli) |
| Package Manager (JS) | pnpm (via Corepack) |
| CI | GitHub Actions (.github/workflows/) |
fosse/
├── fosse.php # Plugin main file + header
├── src/ # Plugin source (PHP) — classmap autoloaded
├── bin/
│ └── build-zip.sh # Builds build/fosse.zip (composer build-zip)
├── bundled/ # Vendored release builds of wordpress-activitypub
│ ├── activitypub/ # and wordpress-atmosphere. Refreshed via
│ └── atmosphere/ # tools/sync-bundled.sh. Do not edit by hand.
├── tests/
│ ├── php/ # PHPUnit tests (WorDBless, *Test.php suffix)
│ │ └── bootstrap.php
│ ├── js/ # Jest tests (*.test.js)
│ └── e2e/ # Playwright specs + Playground blueprint
│ └── blueprint.json
├── tools/ # Isolated composer project for PHPCS (PHP 8.4+)
│ ├── composer.json
│ ├── sync-bundled.sh # Refresh bundled/ from upstream checkouts
│ └── bundled-excludes.txt # Rsync exclude list for sync-bundled.sh
├── sdd/ # Spec-Driven Development docs (per-feature; index in sdd/roadmap.md)
├── .github/
│ ├── workflows/ # tests.yml, linting.yml, e2e.yml, build-zip.yml
│ └── dependabot.yml
├── .phpcs.xml.dist # Jetpack ruleset, text-domain: fosse
├── phpunit.xml.dist
├── playwright.config.ts
├── eslint.config.mjs
├── jest.config.js
├── composer.json # Plugin runtime + phpunit/wordbless dev deps
├── package.json # JS dev deps + scripts
└── AGENTS.md # this file
corepack enable
composer install
composer install --working-dir=tools
pnpm install
pnpm exec playwright install --with-deps chromium # first e2e run onlycomposer run-script test-phpRuns PHPUnit against WordPress booted via WorDBless (dbless engine — no database).
composer run-script lint-php # check
composer run-script fix-php # auto-fixUses the Jetpack PHPCS ruleset from automattic/jetpack-codesniffer installed in tools/. PHPCS requires PHP 8.2+; if your local PHP is older, run it in CI or bump your local PHP.
pnpm test # Jest
pnpm run lint # ESLint
pnpm run lint:fix # ESLint --fix
pnpm run format # Prettier --write
pnpm run format:check # Prettier --checkpnpm run test:e2eBoots WordPress Playground on 127.0.0.1:9400 via the blueprint at tests/e2e/blueprint.json, mounts the repo as the fosse plugin, and runs Playwright specs from tests/e2e/.
composer run-script build-zipProduces build/fosse.zip — a drop-in plugin bundle containing fosse.php, src/, and a production (--no-dev) vendor/. Set FOSSE_VERSION to override the Version: header stamped into the staged fosse.php (e.g. FOSSE_VERSION=0.1.0 composer build-zip). CI (.github/workflows/build-zip.yml) runs the same script to attach the zip to every published release and to refresh a rolling latest-trunk prerelease (tag + release, not a stable build) on each push to trunk.
./tools/sync-bundled.shRe-vendors bundled/activitypub/ and bundled/atmosphere/ from local upstream checkouts. Configure sources via env vars:
FOSSE_AP_SOURCE— path to the wordpress-activitypub checkout (default:~/code/wordpress-activitypub)FOSSE_ATMO_SOURCE— path to the wordpress-atmosphere checkout (default:~/code/wordpress-atmosphere)
The script runs composer update --no-dev --optimize-autoloader inside the Atmosphere source before rsyncing so the vendored copy is self-contained. We use update (not install) because Atmosphere gitignores composer.lock; a stale untracked lock from a previous install would otherwise quietly resurrect dropped dependencies. Atmosphere has no runtime composer deps today (it uses a custom autoloader since #23), so the rsynced vendor/ is mostly composer's own autoload scaffolding — but we keep building it so that if upstream adds runtime deps later, FOSSE picks them up automatically on the next sync. Bundling the federation backends is a short-term bootstrap; long-term we expect to drop this in favor of a cleaner distribution approach.
This project follows WordPress Coding Standards (WPCS) for all PHP code, enforced via the Jetpack PHPCS ruleset.
- Jetpack ruleset (WordPress-Extra + VariableAnalysis + PHPCompatibilityWP + selected MediaWiki sniffs).
- Tabs for indentation.
- Yoda conditions (
if ( null === $var )). - PHPDoc on public/protected methods with
@param,@return. - Text domain:
fosse. - Namespace:
Automattic\Fosse\…(classmap autoload fromsrc/). - Files in
tests/php/are namespacedAutomattic\Fosse\Tests\…via PSR-4 (so*Test.phpnaming is fine;WordPress.Files.FileNameis relaxed there).
@wordpress/eslint-pluginrecommended config.- Prettier formatting (
@wordpress/prettier-config). - Tabs for indentation; single quotes; spaces inside parens (WordPress style).
- PHP: extend
\WorDBless\BaseTestCase. Use@before/#[Before]and@after/#[After](notsetUp/tearDowndirectly). Suffix files withTest.php. - E2E: Playwright
test(...)blocks undertests/e2e/*.spec.ts.
- Imperative mood ("Add X", not "Added X" / "Adds X").
- Component prefix when helpful:
Tests: add smoke test for X.
sdd/roadmap.md indexes every SDD with one-line purpose, status, and Linear/PR pointers — start there to see what's been designed, what's shipped, and what's in flight.
sdd/<feature>/plan.md is the persistent record of what's done — not git log, not Linear. For new SDD plans going forward, each task carries a - **Status**: field, and the top of the file carries a ## Progress checklist mirroring the per-task statuses. Keep both in sync as work progresses. Older plans (e.g. sdd/bundled-backends/) predate this convention and don't need to be retrofitted unless intentionally updated.
Status values:
Not started— default on plan creation.In progress— set when starting a task.✅ Done (<ref>)— set when the task's Verify steps pass.<ref>is a commit SHA, PR number (#123), or upstream PR link; cross-repo tasks link to the merged PR.Skipped (<reason>)— short one-line reason.
Deviations still go in implementation-notes.md (per the SDD workflow); Status is for "did this ship?", implementation-notes is for "what did we actually build vs. the spec?".
Each SDD's spec.md (or plan.md if there's no spec — e.g. settings-page-scoped-actions/, post-type-sync/notes.md) carries a YAML frontmatter status: field that mirrors the roadmap's status column. The frontmatter is the machine-readable truth; the roadmap is the human-readable index.
---
status: planning | in-progress | shipped | archived
---Values:
planning— SDD doc itself isn't merged yet.in-progress— SDD merged; primary deliverables still landing.shipped— Implementation's primary deliverables are live on trunk; the SDD is still actively cross-referenced from sibling SDDs or in-flight work.archived— Shipped, complete, and historical reference. Listed insdd/roadmap.md's## Archivedsection. No follow-up work expected in this area.
Flip both the frontmatter and the roadmap row at the same time. archived is a lightweight mark — the SDD stays in sdd/<feature>/, no files move, so cross-references from AGENTS.md and sibling SDDs keep working.
Run the lint suite at minimum before pushing any branch or opening a PR:
composer run-script lint-php # PHPCS (Jetpack ruleset)
pnpm run format:check # Prettier
pnpm run lint # ESLintThe full CI matrix runs on push, but catching formatting and style failures locally saves a round trip through GitHub Actions — and avoids re-triggering the Copilot PR review bot (and its review-points budget) on every retry push. PHPUnit and E2E can wait for CI; the linters are cheap and should be clean before the first push.
.github/workflows/tests.ymlruns PHPUnit across PHP 8.2/8.3/8.4/8.5 × WP 6.9/trunk. Trunk rows arecontinue-on-error. WP 7.0 covers via thetrunkrow until 7.0 releases, then it gets added as its own column..github/workflows/linting.ymlruns PHPCS (PHP 8.4) and ESLint/Prettier (Node 20). Path filters skip unaffected jobs on PRs..github/workflows/e2e.ymlruns Playwright against Playground..github/workflows/build-zip.ymlbuildsfosse.zipin acontents: readjob, then publishes via separatecontents: writejobs: pushes totrunkrefresh the rollinglatest-trunkprerelease; published releases get the zip attached directly.
- Lint deps live in
tools/composer.json, not root.automattic/jetpack-codesnifferpins recent dependencies that can conflict with plugin-runtime deps; keeping it isolated intools/avoids resolver churn when we add new runtime requirements. - WorDBless copies its
db.phpdrop-in via a composer post-install hook. If tests suddenly fail with wpdb errors, re-runcomposer install. wordpress/is a Composer-managed directory (roots/wordpress). Never edit files inside it —composer installwill overwrite them.- PHPUnit runs with
failOnWarningandfailOnRisky. Output during tests also fails them. Keep tests quiet. - Playground mounts the repo root as the plugin directory. The blueprint expects
fosse.phpto be at repo root; don't move it without updatingtests/e2e/blueprint.jsonandplaywright.config.ts. pnpm install --frozen-lockfilein CI means you must commitpnpm-lock.yamlafter adding/bumping JS deps.bundled/is vendored upstream code. Excluded from PHPCS, PHPUnit, ESLint, Prettier, Jest, and the composer classmap. Don't re-enable those checks for it. Refresh viatools/sync-bundled.sh; never hand-edit files insidebundled/.- Bundled-plugin activation runs on
init, notplugins_loaded. ActivityPub'sactivate()callsflush_rewrite_rules(), which needs$wp_rewrite(initialized oninit).fosse.phpdefers the first-load bootstrap accordingly; don't move it earlier without accounting for that. - Commit
composer.lockalongside everycomposer.jsonchange.bin/build-zip.shrunscomposer validate --no-check-all --no-check-publishbefore install and fails hard on drift. For metadata-only edits (PHP floor,exclude-from-classmap, autoload paths), regenerate withcomposer update --lockrather than a fullcomposer update. The earlier call to untrack the lock for PHP-matrix flexibility went away when we consolidated on PHP 8.2.
Rule of thumb: post-type-agnostic correctness goes upstream; FOSSE-shape-specific behavior stays in FOSSE. If a fix or new hook is useful to any site running wordpress-activitypub or wordpress-atmosphere on its own, land it in that repo — not in bundled/, not as a FOSSE shim. FOSSE then consumes it via tools/sync-bundled.sh.
Worked example from the Bluesky-native-publishing epic:
- Upstream — Atmosphere's
atmosphere_is_short_form_postdiscriminator and ActivityPub'sactivitypub_post_object_typefilter onPost::get_type(). Both describe a universal notion ("is this a short-form post / what AP object type should it become?") and are valuable to any consumer of those plugins. - FOSSE — the
Automattic\Fosse\Object_Typebridge that reads ActivityPub's canonicalactivitypub_object_typeoption and projects it onto Atmosphere's short-form discriminator so both networks agree on the shape. The AP-side filter is no longer registered by FOSSE — ActivityPub reads its own option directly, and the original parallelfosse_object_typeoption was retired insdd/canonical-upstream-options/after the parallel-option pattern proved to leave Atmosphere's UI displaying values it didn't actually use.
See PR #18 (DOTCOM-16812) for the original decision record and sdd/canonical-upstream-options/ for the canonicalization that followed.
Worked example from the long-form Bluesky strategy (DOTCOM-16810):
- Upstream — Atmosphere's
atmosphere_long_form_compositionfilter, thebuild_long_form_records()/build_teaser_thread()/build_truncate_link_text()composition methods onTransformer\Post, theMETA_THREAD_RECORDSpost-meta constant, and the thread-aware redesign ofPublisher::publish/update/delete(sequential writes with rollback + partial-meta writes). Every piece describes a universal "how does Atmosphere compose and persist long posts" concern that's valuable to any consumer ofwordpress-atmosphere. - FOSSE —
Automattic\Fosse\Canonical_Options_Migratorseeds Atmosphere'satmosphere_long_form_compositionwith FOSSE's preferred default ('teaser-thread') on first install when the option is unset. FOSSE no longer keeps a parallelfosse_long_form_strategyoption — Atmosphere reads its own option directly. The original FOSSE-side projector was retired insdd/canonical-upstream-options/for the same reason as the object-type projector.
See sdd/long-form-bluesky-strategy/ for the original strategy decision and sdd/canonical-upstream-options/ for why the projector option was retired.
Canonical_Options_Migrator fires two action hooks operators can wire up for visibility into the one-time legacy-to-canonical migration:
fosse_canonical_migration_conflict— fires when the legacy FOSSE option disagrees with an explicitly-set canonical option. The migration preserves the canonical (visible-UI) value and discards the legacy one; hook here to log, raise an admin notice, or page on disagreement. Args:$key('object_type'or'long_form_strategy'),$legacy,$existing.fosse_canonical_migration_failed— fires when a canonical option write does not converge on the desired value (DB rejection,pre_update_option_*filter intercept, object-cache regression). The migrator preserves the legacy option and skips the completion flag so the next request retries indefinitely. Args:$key,$attempted,$actual.