diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84bf9f78..142c2dd7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,16 +45,24 @@ jobs: java-version: '17' cache: maven - - name: Run guards + - name: Run core architecture and documentation guards + # Every name here must live in graph-compose-core: the run is scoped to + # that module, and Surefire only fails on an empty selection — a name + # that matches nothing while its siblings match is dropped in silence. + # CiGuardListGuardTest asserts each name resolves under core/src/test. + # Guards owned by other modules (PdfRenderInterfaceGuardTest in + # render-pdf, DocumentationExamplesTest and DocumentationSnippetCompileTest + # in qa) run in build-and-test below, which now also covers docs-only PRs. run: | ./mvnw -B -ntp clean \ - "-Dtest=EnginePdfBoundaryTest,CanonicalTemplateComposerPdfBoundaryTest,PdfRenderInterfaceGuardTest,DocumentationCoverageTest,DocumentationExamplesTest,CanonicalSurfaceGuardTest,TemplateComposeApiTest,VersionConsistencyGuardTest" \ + "-Dtest=EnginePdfBoundaryTest,DocumentationCoverageTest,CanonicalSurfaceGuardTest,PackageMapGuardTest,VersionConsistencyGuardTest,CiGuardListGuardTest" \ test -pl :graph-compose-core changes: # Path-based change detection for selective CI on pull requests. Emits the # reverse-dependency flags the heavy jobs gate on: `code` (any build input), - # `core` (the root graph-compose-core module — drives japicmp), `perf` + # `docs` (markdown the guard suites read and compile), `core` (the root + # graph-compose-core module — drives japicmp), `perf` # (modules the smoke benchmark exercises), and `jvm` (published library modules # + toolchain — drives the JDK matrix width). Pushes/dispatch bypass these gates. name: Detect changed paths @@ -62,6 +70,7 @@ jobs: runs-on: ubuntu-latest outputs: code: ${{ steps.filter.outputs.code }} + docs: ${{ steps.filter.outputs.docs }} core: ${{ steps.filter.outputs.core }} perf: ${{ steps.filter.outputs.perf }} jvm: ${{ steps.filter.outputs.jvm }} @@ -86,6 +95,16 @@ jobs: - 'mvnw' - 'mvnw.cmd' - '.github/workflows/ci.yml' + # Markdown is a build input even though it compiles nothing: + # DocumentationSnippetCompileTest compiles the literal java fences in + # docs/, and the guard suites read README / CONTRIBUTING / docs. Kept + # separate from `code` so a docs-only change runs the reactor (which + # carries those guards) without also running examples-generation, + # which installs eight modules and renders the full example catalogue + # to prove nothing about prose. Deliberately absent from `jvm` too, so + # a docs-only PR uses the baseline JDK alone rather than the matrix. + docs: + - '**/*.md' core: - 'core/src/**' - 'core/pom.xml' @@ -119,9 +138,12 @@ jobs: build-and-test: name: Build and run tests (JDK ${{ matrix.java }}) # Selective CI: on a pull request, skip the heavy reactor build + JDK matrix - # when only docs / non-build files changed (`code` is false). Pushes to the - # integration branches and manual dispatch always run the full gate. - if: github.event_name != 'schedule' && (github.event_name != 'pull_request' || needs.changes.outputs.code == 'true') + # when nothing this reactor slice can check has changed. Markdown counts — + # this slice carries the qa guards that compile the published snippets, so a + # docs-only PR runs here (on the baseline JDK alone, since `jvm` stays false) + # rather than merging without any compiler having read the change. Pushes to + # the integration branches and manual dispatch always run the full gate. + if: github.event_name != 'schedule' && (github.event_name != 'pull_request' || needs.changes.outputs.code == 'true' || needs.changes.outputs.docs == 'true') needs: [architecture-and-documentation-guards, changes] runs-on: ubuntu-latest strategy: @@ -158,7 +180,9 @@ jobs: # only via the examples job), the extracted graph-compose-testing module, the # graph-compose-qa cross-module suites (which need the testing module + the # engine test-jar on the classpath and so cannot live in the engine's own test - # scope), and the graph-compose-coverage aggregator (JaCoCo over core + + # scope — this is where DocumentationSnippetCompileTest, the guard that + # compiles the literal markdown fences in docs/, and DocumentationExamplesTest + # actually run), and the graph-compose-coverage aggregator (JaCoCo over core + # render-pdf + templates, counting the qa exec, now with a NON-REGRESSION # RATCHET — this step fails if aggregate INSTRUCTION or BRANCH coverage drops # below the floor in coverage/pom.xml). -am pulls the fonts / emoji upstreams; @@ -189,7 +213,9 @@ jobs: examples-generation: name: Examples Generation Smoke Test - # Follows build-and-test: skipped for a docs-only PR, runs otherwise. + # Gated on `code` alone, deliberately narrower than build-and-test above: + # rendering the example catalogue costs eight module installs and proves + # nothing about a markdown change, so a docs-only PR skips it. if: github.event_name != 'schedule' && (github.event_name != 'pull_request' || needs.changes.outputs.code == 'true') needs: [build-and-test, changes] runs-on: ubuntu-latest @@ -214,9 +240,10 @@ jobs: run: ./mvnw -B -ntp -f fonts/pom.xml -DskipTests install - name: Install graph-compose-emoji (consumed by the examples module) - # The emoji example renders colour emoji from the bundled Noto SVG set; - # like graph-compose-fonts it is a standalone artifact not on Maven - # Central, so install it into the local repo before building examples. + # The emoji example renders colour emoji from the bundled Noto SVG set. + # Like graph-compose-fonts it carries its own version line, so the reactor + # slice never builds it; installing from source pins the examples to this + # tree's asset set rather than the cached jar at the same coordinate. run: ./mvnw -B -ntp -f emoji/pom.xml -DskipTests install - name: Install root artifact (graph-compose-core) @@ -235,22 +262,23 @@ jobs: run: ./mvnw -B -ntp -f wrapper/pom.xml -DskipTests install - name: Install graph-compose-render-docx (consumed by the examples module) - # The DOCX export example depends on the render-docx backend module, which - # is not on Central. Install it after the engine (its graph-compose - # dependency) so the examples build resolves it. + # The DOCX export example depends on the render-docx backend module at + # ${graphcompose.version} — the reactor's -SNAPSHOT, which no repository + # can supply. Install it after the engine (its graph-compose dependency) + # so the examples build resolves it. run: ./mvnw -B -ntp -f render-docx/pom.xml -DskipTests install - name: Install graph-compose-testing (consumed by the examples module) # LayoutSnapshotRegressionExample uses LayoutSnapshotJson and EngineDeckData - # reads benchmark JSON through the jackson it brings; the testing module is - # not on Central, so install it after the engine (its graph-compose - # dependency) before the examples build resolves it. + # reads benchmark JSON through the jackson it brings; the examples pin the + # testing module to the reactor's -SNAPSHOT, so install it after the engine + # (its graph-compose dependency) before the examples build resolves it. run: ./mvnw -B -ntp -f testing/pom.xml -DskipTests install - name: Install graph-compose-render-pptx (consumed by the examples module) # The Engine Deck PPTX example renders the flagship deck through the - # fixed-layout PPTX backend, which is not on Central. Install it after - # the wrapper (its core + render-pdf compile dependencies) AND the + # fixed-layout PPTX backend, pinned to the reactor's -SNAPSHOT version. + # Install it after the wrapper (its core + render-pdf compile dependencies) AND the # testing module — -DskipTests skips execution but Maven still # resolves render-pptx's test-scope graph-compose-testing dependency. run: ./mvnw -B -ntp -f render-pptx/pom.xml -DskipTests install diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6bea0b70..388a40ca 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -50,10 +50,14 @@ jobs: languages: ${{ matrix.language }} queries: security-and-quality - - name: Build (library module only) - # `-pl :graph-compose-core` scopes the analysis to the published artifact and matches - # the canonical verify gate. Examples/benchmarks are intentionally - # excluded — they ship no production code. + - name: Build (engine module) + # Scoped to the engine module alone: no `-am`, so nothing but core is built + # and nothing but core reaches the extractor. Since the 2.0 split + # graph-compose-core is one of several artifacts published to Maven Central, + # which makes this narrower than both the publish train and the canonical + # verify gate in ci.yml — the render backends and templates ship unscanned. + # Widening it to the code-bearing published modules is tracked for 2.2, + # together with a triage pass on the standing alert set. run: ./mvnw -B -ntp -DskipTests -pl :graph-compose-core package - name: Perform CodeQL analysis diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0a3b9014..8c2f1af0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -226,8 +226,10 @@ jobs: - name: Publish render-pptx to Maven Central if: steps.plan.outputs.run_render_pptx == 'true' - # The semantic PPTX backend, lockstep-versioned with the engine (ships on - # the same v* tag). graph-compose-core resolves from the local repo installed + # The PPTX render backend — a fixed-layout POI XSLF backend consuming the + # same resolved LayoutGraph as the PDF one, alongside the older semantic + # manifest skeleton. Lockstep-versioned with the engine (ships on the same + # v* tag). graph-compose-core resolves from the local repo installed # by the engine deploy above. run: ./mvnw -B -ntp -f render-pptx/pom.xml -P release -DskipTests -Dgpg.skip=false deploy env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 558e6688..eaaa8ee6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ follow semantic versioning; release dates are ISO 8601. ## v2.1.1 — Planned +### Build + +- **The CI guard job runs every guard it names.** It selected eight test classes + while scoping the reactor to `graph-compose-core`: two had been deleted months + earlier and two live in `graph-compose-qa` and `graph-compose-render-pdf`, so + four never ran. Surefire aborts only when a selection is entirely empty, so the + four that did match kept the job green. The list is now the five guards that + live in the engine module, and `CiGuardListGuardTest` fails the job if a name in + it stops resolving. +- **A documentation-only pull request is compiled.** Markdown was not a + change-detection input, so a PR touching only `.md` skipped the reactor build + and merged without `DocumentationSnippetCompileTest` ever compiling the java + fences it publishes. Markdown now routes to the reactor slice that carries those + guards, on the baseline JDK, and still skips example generation. +- **`graph-compose-render-pptx` declares PDFBox.** It compiles against + `org.apache.pdfbox` types while declaring only `fontbox`, taking the rest + transitively; the resolved version is unchanged. + ### Fixed - **A heading no longer strands above a block that was asked to stay whole.** diff --git a/core/src/test/java/com/demcha/documentation/CiGuardListGuardTest.java b/core/src/test/java/com/demcha/documentation/CiGuardListGuardTest.java new file mode 100644 index 00000000..62fc8d06 --- /dev/null +++ b/core/src/test/java/com/demcha/documentation/CiGuardListGuardTest.java @@ -0,0 +1,192 @@ +package com.demcha.documentation; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Guards the hand-written {@code -Dtest=} list in the CI guard job against name rot. + * + *
The "Architecture and Documentation Guards" job selects a fixed set of test + * classes by name and runs them with {@code -pl :graph-compose-core}. Surefire + * treats that selection as a whole: it aborts only when every pattern + * matches nothing. A single name that matches nothing while its siblings match is + * dropped without a warning, and the job reports success having run fewer tests + * than it names.
+ * + *Two failure modes therefore pass CI silently. A guard renamed or deleted + * leaves a dead name behind — the job keeps claiming coverage it no longer has. + * A guard that lives in another module ({@code qa}, {@code render-pdf}) can be + * added to the list in good faith and never run, because the module scope + * excludes it. Both had happened here: the list carried two classes deleted + * months earlier plus two that live outside {@code core}, so the job executed + * four of the eight names it advertised.
+ * + *This test parses the workflow and asserts every selected name resolves to a + * test source under {@code core/src/test/java}. It runs inside the very job it + * protects, so a bad edit to that line fails the step that made it. Guards owned + * by other modules belong in the reactor build, not here.
+ */ +class CiGuardListGuardTest { + + private static final Path PROJECT_ROOT = RepoRoot.get(); + private static final Path WORKFLOW = PROJECT_ROOT.resolve(".github/workflows/ci.yml"); + private static final Path CORE_TESTS = PROJECT_ROOT.resolve("core/src/test/java"); + + /** + * The scope that makes a {@code -Dtest=} selection core-only. Anchored at the + * end of the reactor list so {@code -pl :graph-compose-core,:graph-compose-qa} + * — a genuinely multi-module selection — is not held to core residency. + */ + private static final Pattern CORE_SCOPE = + Pattern.compile("-pl :graph-compose-core(?![\\w:,-])"); + + /** + * A {@code -Dtest=} value, quoted or bare, up to the end of the shell word. + * The character class must admit every character Surefire accepts in a + * selector — including the {@code ?} and {@code /} of a path-shaped wildcard + * and the leading {@code !} of an exclusion. A character missing here does not + * fail: it truncates the selector, and the guard then checks a name nobody + * wrote. + */ + private static final Pattern TEST_SELECTION = + Pattern.compile("-Dtest=\"?([A-Za-z0-9_$.,*?/!+#\\[\\]-]+)\"?"); + + /** Workflow steps start at a {@code - name:} key at any indentation. */ + private static final Pattern STEP_BOUNDARY = Pattern.compile("(?m)^\\s*- name:"); + + @Test + void everyGuardNamedInTheCoreScopedCiStepLivesInCore() throws IOException { + List