Skill: audit an OSGi plugin for ES → OpenSearch cutover breakage
@fabrizzio-dotCMS is already working on this.
Since Sep 8, 2026.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Description
The ES → OpenSearch migration deprecated 20 symbols with @Deprecated(forRemoval = true) plus one plain @Deprecated, and the removal is hard: ESContentTool's javadoc names v26.08.04 (R7 / dotEvergreen cutover), and the Indicies* stack goes "once we complete migration to OpenSearch 3.x". Any OSGi plugin still compiled against those symbols fails to compile at that point, and a subset fails silently before that — the Velocity raw-response case returns a different string with no error.
Today the only way for a plugin author (or for us, auditing a customer's plugin during an upgrade) to find out is to read docs/backend/SEARCH_API_MIGRATION.md and grep by hand. That does not scale across the plugin ecosystem, and it misses the cases that are not a simple symbol match.
Ask: a skill that takes an OSGi plugin — source directory, or a built .jar with no sources — and reports every place it will break at the OpenSearch cutover, with severity, the exact replacement signature, and a file:line reference.
The full replacement mapping already exists and is verified against main: docs/backend/ES_OS_DEPRECATIONS.md. The skill should read its rules from that document (or a machine-readable file generated from it) rather than restating them, so there is one place to update when the inventory moves.
What it has to detect
Grouped by how it has to look, because a plain grep only covers the first group.
1. Direct symbol references (grep-able)
| Symbol | Replacement |
|---|---|
ContentletAPI.esSearch(String, boolean, User, boolean) |
search(...) → ContentSearchResults<Contentlet> |
ContentletAPI.esSearchRaw(String, boolean, User, boolean) |
searchRaw(...) → ContentSearchResponse |
APILocator.getEsSearchAPI() |
APILocator.getSearchAPI() |
APILocator.getIndiciesAPI() |
APILocator.getVersionedIndicesAPI() |
IndiciesAPI / IndiciesInfo / IndiciesFactory |
VersionedIndicesAPI / VersionedIndices / IndicesFactory |
ContentletIndexAPI.timestampFormatter |
threadSafeTimestampFormatter |
SiteSearchAPI.getFacets(String, String) |
getAggregations(String, String) |
2. Hook overrides — a plugin implementing ContentletAPIPreHook / ContentletAPIPostHook and overriding esSearch / esSearchRaw. The replacements (search / searchRaw) have no-op defaults, so the plugin keeps compiling but its interception silently stops firing once core calls the new path. This one is easy to miss and high impact.
3. Vendor imports — any import org.elasticsearch.*. At Phase 3 the ES client is gone. Also com.dotcms.content.elasticsearch.business.ESSearchResults, and the three duplicated stats classes (IndexStats, ClusterStats, NodeStats) where the old and new names coexist — importing the old one compiles today and breaks later.
4. Velocity templates — $ESContent.esSearch(...) and $ESContent.esRaw(...), plus the silent break: $ESContent.raw($q).toString() no longer yields ES wire-format JSON, and $results.hits / $results.response / $results.aggregations no longer resolve. Templates ship inside plugins, so a Java-only scan misses these entirely.
5. Internal-API dependence — CacheLocator.getIndiciesCache(), FactoryLocator.getIndiciesFactory(), anything under com.dotcms.content.index.*, the *ES/*OS implementations, the routers. These carry no deprecation annotation, so the compiler never warned and there is no migration path — the plugin bet on something core never promised. Worth reporting as its own severity rather than as a deprecation.
6. Semantic changes that are not a rename — cases where swapping the symbol is not enough and the report has to say so:
IndiciesAPI.point(IndiciesInfo)→VersionedIndicesAPI.saveIndices(VersionedIndices)throwsDotDataExceptionwhen the object carries no version. The old API accepted anything.IndiciesAPI.loadIndicies()→loadIndices(String version)takes a version argument and returns anOptional, ignoring legacy null-version rows.ESSearchResults→ContentSearchResults<T>:getTotalResults()replacesgetResponse().getHits().getTotalHits().value.ElasticsearchException→DotSearchException: still unchecked, so only explicitcatchblocks need editing.
7. Jar-only input — for a plugin with no sources available, resolve references from the bytecode (javap -p -c, or the class constant pool) and from the OSGi manifest's Import-Package header, which is where org.elasticsearch.* shows up declaratively.
Proposed severity model
The report is useless if everything is "warning". Four levels, each with a different action:
| Severity | Meaning | Example |
|---|---|---|
BLOCKER |
Will not compile after removal | esSearch, IndiciesAPI |
SILENT |
Compiles, wrong behaviour at runtime, no error | raw().toString(), hook overrides that stop firing |
ADVISORY |
Deprecated but still resolves; no break yet | ES_* config keys (they fall back) |
UNSUPPORTED |
Depends on an internal API with no migration path | CacheLocator.getIndiciesCache(), content.index.* |
Acceptance Criteria
- Skill invoked against a plugin source directory reports every finding with severity,
file:line, the deprecated symbol, and the replacement signature - Skill invoked against a built
.jarwith no sources produces the same findings for groups 1, 2, 3 and 5 (bytecode +Import-Package) - Velocity templates inside the plugin are scanned, including the
raw(...).toString()silent case - Hook overrides of
esSearch/esSearchRaware reported asSILENT, not asBLOCKER— they compile, they just stop firing - The four semantic-change cases emit their caveat, not just the replacement name
- A fixture plugin exercising every deprecated symbol yields a finding for each one (no misses)
- A fixture plugin already migrated to the neutral API yields zero findings (no false positives)
- Rules are read from
docs/backend/ES_OS_DEPRECATIONS.mdor a file generated from it — adding an entry to the inventory needs no skill edit - Report is emitted in a form usable both by a human and by CI (non-zero exit when any
BLOCKERis present) - Skill passes
just skills-lint
Priority
Medium — but time-boxed: the value is preventive and drops sharply after the v26.08.04 cutover.
Additional Context
Naming — decided: dot-plugin-os-audit. The approved domains in .claude/skills/skills.config.json (dot-issue-, dot-pr-, dot-release-, dot-cicd-, dot-content-, dot-ui-) have no home for plugin tooling, so the implementing PR must add dot-plugin- to approvedDomains in the same PR — CONTRIBUTING §1 states that edit is the review point for "is this a real new domain?". The domain is expected to be reusable for other plugin-ecosystem tooling.
Considered and rejected: dot-content-audit-plugin, which avoids the config change but reads as auditing content rather than plugins.
Related existing skills (both core-facing, neither audits third-party plugins — mark as related, do not fork):
es-os-router-pattern— implementing thePhaseRouter<T>pattern inside coreextract-neutral-api— refactoring a vendor pair into a neutral contract
Source material:
docs/backend/ES_OS_DEPRECATIONS.md— the full verified inventory, 47 entries, with file:line and originating commitdocs/backend/SEARCH_API_MIGRATION.md— the plugin-facing migration guidedocs/backend/OPENSEARCH_MIGRATION.md— phase architecture, why Phase 3 has no fallback
Why now: the audit is most valuable before the cutover, while plugin authors still have a window. After v26.08.04 the skill stops being a preventive tool and becomes a post-mortem.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.