Skip to content

Release v2.9.0

Latest

Choose a tag to compare

@github-actions github-actions released this 27 Aug 11:58

Added

  • content_validate action on statamic-content-facade — Validates content that is already stored against its blueprints. Writes through this addon are validated on the way in; content that arrives another way (git merges, hand-edited YAML, blueprints changed after the content was written) was previously invisible. Each record gets two passes: the blueprint's own validation rules, evaluated the same way a Control Panel save evaluates them, plus structural checks the rule engine cannot express — replicator/bard blocks naming a set that no longer exists, sets and grid rows storing keys the blueprint dropped, select/radio/button_group/checkboxes values outside the declared options, assets fields pointing at missing files, and navigation items linking to deleted entries. All of these pass rule validation silently while breaking at render time. Supports scope, collection/taxonomy filters, severity filtering, offset/limit paging across the combined record stream, and a max_findings cap that keeps summary counts accurate when the list is truncated

  • ValidatesContentRecords concern — The two-pass record validation above, extracted so other routers can reuse it. The rule pass is isolated per record: a malformed stored value that makes a fieldtype's rule builder throw is reported as a rule_engine_error warning rather than aborting the sweep, and the structural pass still runs to name the underlying shape problem

  • MCP resources for blueprints — The server now exposes the resources capability, which it previously left unused. statamic://blueprints lists every readable blueprint with its URI; statamic://blueprints/{namespace}/{handle} returns one blueprint's fields, so a client can look up field structure to shape a write without spending a tool call. Because RequireMcpPermission defers scope checks to the primitive, resources run the same four gates a router read does — tool enablement, token scope (blueprints:read), resource policy, and Statamic permissions — via a new AuthorizesResourceAccess concern. Blueprints the resource policy hides are absent from the index, not merely refused on read

  • #[Title] on every tooltools/list has always carried a title field; without the attribute it fell back to Str::headline(class_basename()), so clients displayed "Entries Router". Tools now declare their own display titles

  • Typed validation modelFinding, RecordRef, and the Severity/FindingType/RecordType enums replace the array<string, mixed> bags the validation sweep threaded through its call chain. Severity is derived from the finding type rather than passed alongside it, so a finding cannot be built with a severity that contradicts what it describes; findings become arrays only at the MCP response boundary

  • Testing/InteractsWithMcp and Testing/FakeTransport — Shipped testing helpers for driving this addon's MCP server, dogfooded by the package's own suite. A tests/Fixtures composition site is included in the PHPStan paths so the traits are analysed where they are actually mixed in — which immediately caught a wrong class-string bound in the trait itself

  • Supply-chain gatebin/check-licenses.php fails the build on any non-permissive dependency (SPDX dual-licensing handled: a package passes if any arm is permissive), and bin/generate-sbom.php emits a deterministic CycloneDX 1.5 sbom.json with sorted components and a content-derived serial number, so it only changes when dependencies do. Wired into CI along with composer audit --no-dev, plus a new composer qa aggregate. CI validates the generated document rather than diffing it against the committed one: composer.lock is gitignored because this is a library, so a freshly resolved lock legitimately differs and a drift check would fail whenever any transitive dependency publishes. The license check found one real case: statamic/cms is proprietary, recorded as a justified exception because a Statamic addon cannot avoid depending on Statamic

  • SECURITY.md — Private vulnerability reporting via GitHub, an explicit split between what this addon secures and what the operator does, and a limitations section stating plainly that the audit log is append-only by convention with no hash chain (neither tamper-proof nor tamper-evident), that confirmation tokens are replayable within their window, and that require_https falls back to off when the published config predates the key

Fixed

  • The MIT license text actually shipscomposer.json has always declared MIT, but the repository never contained a LICENSE file, so the grant existed only as metadata. The standard MIT text is now included
  • Entry updates no longer fail on blueprints with a required slug (#39) — The #27 fix removed the slug from the validated payload entirely, so Statamic's default slug field (validate: [required, UniqueEntryValue…]) could never be satisfied: every update failed with "The Slug field is required", whether the caller omitted the slug or resent the current one. The entry's effective slug is now injected back into the validation payload, and both FieldsValidator invocations (including the TypeError fallback) resolve the UniqueEntryValue({collection}, {id}, {site}) placeholders via withReplacements(), so the rule excludes the entry being updated — the false positive #27 was about — while a slug owned by another entry is still rejected
  • date no longer has to be resent on every update of a dated collection — Like the slug, the date is an entry property absent from the merged data payload, so a blueprint with a required date field failed any update that did not repeat a date the caller never meant to change. The entry's current date now satisfies the rule when the payload omits it
  • Explicit slug on create actually workscreateEntry() read $arguments['slug'], but the tool schema never declared the parameter, so no client could send it and the slug was always derived from the title. The schema now declares slug, and create also accepts it as data.slug — the shape update uses — storing it as an entry property in both cases, never as a data key
  • PHPStan level 9 is no longer partly disabled — The config carried blanket ignoreErrors patterns (#Method .* should return .* but returns mixed#, #Cannot call method .* on .*\|null#, #Parameter .* expects .*, mixed given#, and four more) that suppressed whole error classes across src/, so "level 9 clean" meant considerably less than it sounded. Removing them surfaced 54 real errors — almost all method calls on Blueprint|null, Entry|null, or GlobalSet|null after a lookup, because requireResource() returned an error array without narrowing the variable. Every site now checks for null explicitly (identical messages, identical behaviour, and the analyser can see it), the untyped Statamic/Eloquent return values are narrowed rather than cast, and the four @phpstan-ignore annotations on abort() calls are gone. requireResource() itself is removed, having no callers left
  • CI verifies formatting instead of rewriting it — The Tests workflow ran Pint in fix mode, committed the result, and pushed it back to the branch; it now runs pint --test and fails on violations, matching what the release workflow already does
  • CI exercises both Laravel majorscomposer.json claims Laravel 12 and 13 via orchestra/testbench: ^10.0 || ^11.0, but the matrix only varied PHP, so every job resolved testbench 11 and the Laravel 12 claim was never verified. The matrix now spans both. The suite passes on Laravel 12
  • Package classes are no longer final — Nine classes were sealed, blocking consumers from extending or decorating what the package ships
  • Failed tool calls set the MCP isError flag — Every response was returned via Response::structured(), which never marks an error, so a failed call arrived at the client indistinguishable from a successful one; only a model parsing the JSON body would notice "success": false. Failures are now assembled from Response::error() plus the same structured content, so both the protocol flag and the full envelope (including confirmation tokens) survive
  • Plaintext credentials no longer reach stack tracesTokenService::validateToken()/findByPlainText(), ConfirmationTokenManager's token methods, AuthenticateForMcp::authenticateWithCredentials(), and every ClientConfigGenerator method took secrets as plain parameters. In the stdio server, setupErrorHandling() writes getTraceAsString() to stderr, so a throw anywhere in those call chains logged the bearer token verbatim. All are now marked #[\SensitiveParameter], matching the hardening laravel/mcp applied upstream in v0.9.0
  • Server version no longer hardcodedStatamicMcpServer::$version was the literal '2.8.0' and would have drifted at the next release. It is read from Composer's installed-package metadata, falling back to 0.0.0 only when that is unavailable
  • Release workflow no longer hangs — The release job ran pest --parallel, which is not parallel-safe: Statamic\Testing\AddonTestCase points every Stache store, and PreventsSavingStacheItemsToDisk's dev-null directory, at one shared tests/__fixtures__ path, so ParaTest workers deleted each other's fixtures. This produced ~34 spurious failures or, when workers collided on the file-store flock() calls, a hang that burned the 6h job timeout (v2.6.1 and v2.8.0 both died this way, and both releases had to be published by hand). The release job now runs the same single-process pest that gates pull requests, and every job has an explicit timeout-minutes so a hang fails in minutes instead of hours
  • Release notes are no longer empty — The changelog extraction matched [v2.8.0] against headings written as [2.8.0], so it never selected anything. The tag's v prefix is now stripped, with a fallback message if the section is missing
  • Release workflow verifies formatting instead of rewriting it — The Fix code formatting step ran Pint in fix mode and discarded the result; it now runs pint --test and fails on violations

Changed

  • Docs follow the standard topic layoutintroduction.md became index.md, quickstart.md moved to the docs root, and a generated requirements.md states only what the resolver enforces. docs/superpowers/ and its subfolders gained the _index.md landings and frontmatter they were missing, which had been downgrading the docs site's grading from complete to partial. All relative links repaired and verified
  • Pest constraint widened to ^4.1 || ^5.0 — Pest 5 is stable; the package was a major behind
  • Tests now exercise the real MCP protocol — Every test drove tools through execute() directly, so JSON-RPC argument delivery, response serialization, the isError flag, and outputSchema conformance had no coverage at all; StatamicMcpServerTest even read protected properties by reflection. A new McpProtocolSurfaceTest drives tools through StatamicMcpServer::tool(). This required registering Laravel\Mcp\Server\McpServiceProvider in TestCase::getPackageProviders() — Testbench does not run package auto-discovery, so without it Laravel\Mcp\Request never receives arguments and protocol-level tests pass while asserting nothing
  • composer stan passes --memory-limit=2G — PHPStan crashed its parallel worker at PHP's default 128M; 1G proved marginal once tests/Fixtures joined the analysis paths
  • Removed the composer test:parallel script — It could not work for the reason above; use composer test