LintSiteMojo.java
package network.ike.docs.plugin;
import org.apache.maven.api.plugin.MojoException;
import org.apache.maven.api.plugin.annotations.Mojo;
import org.apache.maven.api.plugin.annotations.Parameter;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Lint {@code src/site/site.xml} for IKE Network theme + breadcrumb
* conventions (ike-issues#319).
*
* <p>The shared site assets ({@code site.css}, {@code ike-logo.svg})
* are consolidated in {@code ike-doc-resources} (#318). The
* per-project {@code site.xml} legitimately stays local — breadcrumbs
* and menus genuinely vary — but it carries a small set of
* conventions that have drifted in the past:
*
* <ul>
* <li><b>{@code <bodyClass>}</b> must be {@code sentry-green}
* (Forest theme) for ike.network consumers, {@code sentry-purple}
* (Horizon) for knowledge.design. The 5 ike-docs submodules
* shipped with {@code sentry-purple} after {@code ike-platform}
* and {@code ike-tooling} switched to {@code sentry-green}
* (per #307), producing a purple banner at
* https://ike.network/ike-docs/ike-doc-resources/.</li>
* <li><b>Breadcrumb leading item</b> — those same 5 files carried a
* stale {@code <item name="IKE Pipeline" href="../index.html"/>}
* from before the ike-pipeline → ike-docs/ike-platform repo
* split (#216).</li>
* <li><b>{@code <skin>} GAV</b> — drift here is silent until you
* upgrade.</li>
* </ul>
*
* <p>The ike.network ↔ knowledge.design boundary means a single
* global enforcement isn't right; the expected {@code bodyClass} is
* per-deployment. Set the deployment target via the
* {@code ike.site.deployment} property (defaults to {@code ike-network}).
*
* <p>When {@code src/site/site.xml} doesn't exist, this goal is a
* silent no-op — modules without a custom site descriptor inherit
* from the parent and don't need linting.
*
* <p>Skip with {@code -Dike.skip.lint-site=true}.
*
* @see <a href="https://github.com/IKE-Network/ike-issues/issues/319">ike-issues#319</a>
*/
@Mojo(name = "lint-site", defaultPhase = "validate")
public class LintSiteMojo implements org.apache.maven.api.plugin.Mojo {
@org.apache.maven.api.di.Inject
private org.apache.maven.api.plugin.Log log;
/**
* Access the Maven logger.
*
* @return the logger
*/
protected org.apache.maven.api.plugin.Log getLog() { return log; }
/**
* Path to the project's site descriptor. Default is the
* conventional location; override for non-standard layouts.
*/
@Parameter(property = "ike.lint-site.path",
defaultValue = "${project.basedir}/src/site/site.xml")
File siteXml;
/**
* Deployment target — controls the expected {@code <bodyClass>}.
*
* <ul>
* <li>{@code ike-network} (default) → expects {@code sentry-green}</li>
* <li>{@code knowledge-design} → expects {@code sentry-purple}</li>
* </ul>
*
* Override in projects deployed to knowledge.design with
* {@code -Dike.site.deployment=knowledge-design}.
*/
@Parameter(property = "ike.site.deployment",
defaultValue = "ike-network")
String deployment;
/**
* Expected {@code <skin>} groupId. Drift here causes silent skin
* changes when the project's pluginManagement-inherited version
* is older than the locally-declared one.
*/
@Parameter(property = "ike.lint-site.skin-group",
defaultValue = "org.sentrysoftware.maven")
String expectedSkinGroup;
/**
* Expected {@code <skin>} artifactId.
*/
@Parameter(property = "ike.lint-site.skin-artifact",
defaultValue = "sentry-maven-skin")
String expectedSkinArtifact;
/**
* Skip linting entirely. Use sparingly — the conventions checked
* here have all caused production-site drift at least once.
*/
@Parameter(property = "ike.skip.lint-site", defaultValue = "false")
boolean skip;
/** Creates this goal instance. */
public LintSiteMojo() {}
@Override
public void execute() throws MojoException {
if (skip) {
getLog().info("idoc:lint-site skipped "
+ "(-Dike.skip.lint-site=true)");
return;
}
if (!siteXml.isFile()) {
getLog().debug("idoc:lint-site: no site.xml at "
+ siteXml + " — module inherits from parent. Skipping.");
return;
}
String content;
try {
content = Files.readString(siteXml.toPath(),
StandardCharsets.UTF_8);
} catch (IOException e) {
throw new MojoException(
"Could not read " + siteXml + ": " + e.getMessage(),
e);
}
List<String> problems = lint(content, deployment,
expectedSkinGroup, expectedSkinArtifact);
if (problems.isEmpty()) {
getLog().debug("idoc:lint-site: " + siteXml
+ " — all checks passed.");
return;
}
StringBuilder msg = new StringBuilder();
msg.append("idoc:lint-site found ").append(problems.size())
.append(" problem(s) in ").append(siteXml).append(":\n");
for (String p : problems) {
msg.append(" ✗ ").append(p).append("\n");
}
msg.append("\n Fix the site.xml and re-run, or skip with "
+ "-Dike.skip.lint-site=true (not recommended — these "
+ "conventions have all caused production drift).");
throw new MojoException(msg.toString());
}
/**
* Apply all site.xml lint rules to the given content. Pure
* function for testability — does no I/O.
*
* @param siteXmlContent raw site.xml as a string
* @param deployment deployment target
* ({@code ike-network} or
* {@code knowledge-design})
* @param expectedSkinGroup expected {@code <skin>} groupId
* @param expectedSkinArtifact expected {@code <skin>} artifactId
* @return list of problem messages; empty if all checks passed
*/
public static List<String> lint(String siteXmlContent,
String deployment,
String expectedSkinGroup,
String expectedSkinArtifact) {
List<String> problems = new ArrayList<>();
// ── Check 1: <bodyClass> ─────────────────────────────────
String expectedBodyClass = expectedBodyClassFor(deployment);
Matcher bodyClassMatcher = Pattern.compile(
"<bodyClass>\\s*([^<]+?)\\s*</bodyClass>")
.matcher(siteXmlContent);
if (bodyClassMatcher.find()) {
String actual = bodyClassMatcher.group(1).trim();
if (!actual.equals(expectedBodyClass)) {
problems.add("<bodyClass>: expected '" + expectedBodyClass
+ "' (deployment=" + deployment + "), found '"
+ actual + "'");
}
} else {
problems.add("<bodyClass>: missing — expected '"
+ expectedBodyClass + "' (deployment="
+ deployment + ")");
}
// ── Check 2: breadcrumb deny-list ─────────────────────────
// Stale labels left over from repo splits or rebrands. The
// pattern matches <item name="X" ...> inside the <body> /
// <breadcrumbs> region but we don't need fine-grained
// anchoring — any item with one of these names is wrong
// regardless of location.
for (String denyName : STALE_BREADCRUMB_NAMES) {
Pattern denyPattern = Pattern.compile(
"<item\\s+[^>]*name=[\"']"
+ Pattern.quote(denyName) + "[\"']");
if (denyPattern.matcher(siteXmlContent).find()) {
problems.add("breadcrumb: stale name '" + denyName
+ "' (left over from a pre-split layout; "
+ "see #216 for the ike-pipeline → "
+ "ike-docs/ike-platform rename)");
}
}
// ── Check 3: <skin> GAV ──────────────────────────────────
Matcher skinGroupMatcher = Pattern.compile(
"<skin>\\s*<groupId>\\s*([^<]+?)\\s*</groupId>")
.matcher(siteXmlContent);
Matcher skinArtifactMatcher = Pattern.compile(
"<skin>\\s*<groupId>[^<]+</groupId>\\s*"
+ "<artifactId>\\s*([^<]+?)\\s*</artifactId>")
.matcher(siteXmlContent);
if (!skinGroupMatcher.find()
|| !skinGroupMatcher.group(1).trim().equals(expectedSkinGroup)) {
String actual = skinGroupMatcher.reset().find()
? skinGroupMatcher.group(1).trim() : "(missing)";
problems.add("<skin>/<groupId>: expected '" + expectedSkinGroup
+ "', found '" + actual + "'");
}
if (!skinArtifactMatcher.find()
|| !skinArtifactMatcher.group(1).trim().equals(expectedSkinArtifact)) {
String actual = skinArtifactMatcher.reset().find()
? skinArtifactMatcher.group(1).trim() : "(missing)";
problems.add("<skin>/<artifactId>: expected '"
+ expectedSkinArtifact + "', found '" + actual + "'");
}
return problems;
}
/**
* Return the expected {@code <bodyClass>} for a deployment target.
*
* @param deployment one of {@code ike-network} or
* {@code knowledge-design}
* @return the expected {@code bodyClass} string
*/
public static String expectedBodyClassFor(String deployment) {
if ("knowledge-design".equals(deployment)) {
return "sentry-purple";
}
// Default + explicit ike-network → Forest theme.
return "sentry-green";
}
/**
* Breadcrumb names that are known-stale and should never appear
* in a current IKE site descriptor. Extend this list when a
* rebrand or split retires a label.
*/
static final List<String> STALE_BREADCRUMB_NAMES = List.of(
"IKE Pipeline" // #216 ike-pipeline → ike-docs/ike-platform
);
}