511 lines
24 KiB
Markdown
511 lines
24 KiB
Markdown
# Release Hygiene Refactoring Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Make every release-hygiene path truthful by fixing the sample-off architecture gate, aligning the Gradle 9.0.0 wrapper and CI validation, making Docker cache stages valid without `.git`, completing SpotBugs analysis classpaths, and removing the observed Gradle 10 deprecation.
|
|
|
|
**Architecture:** Leaf-specific architecture rules move to their owning leaf while root tests remain cross-module. Build inputs become explicit: Docker copies registry inputs, evidence-only Git validation executes only in evidence tasks, wrapper bytes/checksums are fixed, and SpotBugs derives auxiliary inputs from the source set it analyzes.
|
|
|
|
**Tech Stack:** Java 21, Spring Boot 4.0.0, Gradle 9.0.0 Groovy DSL, ArchUnit 1.3.0, SpotBugs Gradle plugin 6.5.6/SpotBugs 4.10.2, Bash, Docker/BuildKit, GitHub Actions.
|
|
|
|
## Global Constraints
|
|
|
|
- Preserve all 19 leaf identities and production dependency edges from `src/config/architecture/modules.json`.
|
|
- `domain-core` and `application-core` gain no framework, transport, database, or cloud dependency.
|
|
- Do not weaken an architecture rule with a global `allowEmptyShould(true)`.
|
|
- Keep Gradle at exactly `9.0.0` in this plan.
|
|
- Set `distributionSha256Sum=8fad3d78296ca518113f3d29016617c7f9367dc005f932bd9d93bf45ba46072b`.
|
|
- The official Gradle 9.0.0 wrapper JAR SHA-256 is `76805e32c009c0cf0dd5d206bddc9fb22ea42e84db904b764f3047de095493f3`.
|
|
- Pin `gradle/actions/wrapper-validation` to commit `3f131e8634966bd73d06cc69884922b02e6faf92` in workflows that invoke Gradle.
|
|
- Docker images do not receive `.git`; full evidence revisions arrive through `-PgitRevision`/CI attestation.
|
|
- SpotBugs dependency scopes are not widened to silence missing-class output.
|
|
- Agents do not stage, commit, amend, or push; commit steps from the generic workflow are replaced by diff/status evidence.
|
|
|
|
---
|
|
|
|
### Task 1: Move the Object Storage Architecture Rule to Its Owning Leaf
|
|
|
|
**Files:**
|
|
- Create: `src/adapter/outbound/objectstorage/src/test/java/dev/caskeleton/adapter/outbound/objectstorage/ObjectStorageArchitectureTest.java`
|
|
- Modify: `src/adapter/outbound/objectstorage/build.gradle`
|
|
- Modify: `src/adapter/outbound/objectstorage/gradle.lockfile`
|
|
- Modify: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:1586-1608`
|
|
|
|
**Interfaces:**
|
|
- Consumes: production classes under `dev.caskeleton.adapter.outbound.objectstorage..` and application/shared contracts already on the Object Storage test classpath.
|
|
- Produces: an owner-local ArchUnit rule named `OBJECT_STORAGE_ADAPTER_METHOD_RETURNS_ONLY_APPLICATION_OR_PRIMITIVES`; a sample-off root suite with no Object Storage presence requirement.
|
|
|
|
- [ ] **Step 1: Reproduce the existing failing regression**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cd src
|
|
./gradlew :app-bootstrap:sampleOffTest --tests '*CleanArchitectureTest' --console=plain
|
|
```
|
|
|
|
Expected: FAIL only at `OBJECT_STORAGE_ADAPTER_METHOD_RETURNS_ONLY_APPLICATION_OR_PRIMITIVES` because no matching classes are present.
|
|
|
|
- [ ] **Step 2: Add the owner-local test before removing the root rule**
|
|
|
|
Create a package-local ArchUnit test that imports production classes from the Object Storage package and applies this rule:
|
|
|
|
```java
|
|
@AnalyzeClasses(packages = "dev.caskeleton.adapter.outbound.objectstorage")
|
|
class ObjectStorageArchitectureTest {
|
|
@ArchTest
|
|
static final ArchRule OBJECT_STORAGE_ADAPTER_METHOD_RETURNS_ONLY_APPLICATION_OR_PRIMITIVES =
|
|
methods()
|
|
.that()
|
|
.areDeclaredInClassesThat()
|
|
.resideInAPackage("..adapter.outbound.objectstorage..")
|
|
.and()
|
|
.areDeclaredInClassesThat()
|
|
.haveSimpleNameEndingWith("Adapter")
|
|
.and()
|
|
.arePublic()
|
|
.and()
|
|
.areNotStatic()
|
|
.should()
|
|
.notHaveRawReturnType(
|
|
JavaClass.Predicates.resideInAnyPackage(
|
|
"..adapter.outbound..",
|
|
"..adapter.inbound.web..",
|
|
"..adapter.outbound.persistence.."))
|
|
.allowEmptyShould(false);
|
|
}
|
|
```
|
|
|
|
Add the owner-local test dependency:
|
|
|
|
```groovy
|
|
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
|
|
```
|
|
|
|
Refresh only the Object Storage leaf lock state with its existing `resolveAndLockAll --write-locks`
|
|
task. This is a test-scope dependency; do not add a production project or external dependency edge.
|
|
|
|
- [ ] **Step 3: Run the owner test while the root regression remains red**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cd src
|
|
./gradlew :adapter:outbound:objectstorage:resolveAndLockAll --write-locks --console=plain
|
|
./gradlew :adapter:outbound:objectstorage:test --tests '*ObjectStorageArchitectureTest' --console=plain
|
|
```
|
|
|
|
Expected: PASS with matching production adapter methods.
|
|
|
|
- [ ] **Step 4: Remove only the misplaced root rule**
|
|
|
|
Delete the `OBJECT_STORAGE_ADAPTER_METHOD_RETURNS_ONLY_APPLICATION_OR_PRIMITIVES` field from `CleanArchitectureTest`; do not change neighboring cross-module rules.
|
|
|
|
- [ ] **Step 5: Verify both ownership paths**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cd src
|
|
./gradlew :adapter:outbound:objectstorage:test :app-bootstrap:sampleOffTest --console=plain
|
|
```
|
|
|
|
Expected: PASS, zero failed tests.
|
|
|
|
- [ ] **Step 6: Record diff evidence without committing**
|
|
|
|
Run `git diff --check` and `git status --short`; retain the output for the task review.
|
|
|
|
### Task 2: Align and Validate the Gradle 9.0.0 Wrapper
|
|
|
|
**Files:**
|
|
- Create: `.github/scripts/verify-gradle-wrapper.sh`
|
|
- Modify: `src/gradle/wrapper/gradle-wrapper.properties`
|
|
- Regenerate: `src/gradle/wrapper/gradle-wrapper.jar`, `src/gradlew`, `src/gradlew.bat`
|
|
- Modify: `.github/workflows/ci-quality-gates.yml`
|
|
- Modify: `.github/workflows/dependency-vulnerability.yml`
|
|
- Modify: `.github/workflows/jpa-r2-evidence.yml`
|
|
- Modify: `.github/workflows/object-storage-qualification.yml`
|
|
- Modify: `.github/workflows/redis-production-readiness.yml`
|
|
- Lock without modification: `.github/workflows/link-check.yml`
|
|
- Modify: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/DeveloperExperienceContractTest.java`
|
|
|
|
**Interfaces:**
|
|
- Consumes: repository root as argument 1, wrapper properties/JAR, and every YAML workflow under `.github/workflows`.
|
|
- Produces: executable `verify-gradle-wrapper.sh` with exit 0 only for the exact Gradle 9.0.0 wrapper, the reviewed six-file workflow path/SHA-256 lock, the repository's restricted canonical workflow grammar, and jobs where an unconditional pinned validation step gates every reachable Gradle invocation.
|
|
|
|
- [ ] **Step 1: Write failing executable-contract tests**
|
|
|
|
Add a `DeveloperExperienceContractTest` case that runs:
|
|
|
|
```java
|
|
Process process =
|
|
new ProcessBuilder("bash", ".github/scripts/verify-gradle-wrapper.sh", REPOSITORY_ROOT.toString())
|
|
.directory(REPOSITORY_ROOT.toFile())
|
|
.redirectErrorStream(true)
|
|
.start();
|
|
assertThat(process.waitFor()).as(new String(process.getInputStream().readAllBytes(), UTF_8)).isZero();
|
|
```
|
|
|
|
Add a second case that copies wrapper properties/JAR and workflows to `@TempDir`, changes the distribution checksum, runs the script against that fixture root, and asserts a non-zero exit. The production mutation this test catches is accepting a wrong wrapper or distribution checksum.
|
|
|
|
- [ ] **Step 2: Verify RED**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cd src
|
|
./gradlew :app-bootstrap:test --tests '*DeveloperExperienceContractTest' --console=plain
|
|
```
|
|
|
|
Expected: FAIL because `.github/scripts/verify-gradle-wrapper.sh` does not exist and the checked-in wrapper is not the Gradle 9.0.0 JAR.
|
|
|
|
- [ ] **Step 3: Implement the wrapper verifier**
|
|
|
|
The Bash script must:
|
|
|
|
```text
|
|
1. require exactly one repository-root argument;
|
|
2. require the exact ordered eight-line wrapper-properties file, including the Gradle 9.0.0 URL
|
|
and distribution checksum from Global Constraints;
|
|
3. reject duplicate, alternate-separator, escaped, continued, reordered, or extra properties;
|
|
4. compare the wrapper JAR SHA-256 with the exact Gradle 9.0.0 JAR hash;
|
|
5. enumerate every top-level `.yml`/`.yaml` workflow, reject symlinks/special files, and compare the
|
|
exact sorted six-path set and SHA-256 values to the verifier's embedded reviewed workflow lock;
|
|
additions, removals, renames, or byte changes are failures;
|
|
6. structurally validate the supported block grammar before admission and emit specific diagnostics
|
|
for recognized noncanonical `jobs`/job/`steps` containers, flow collections, aliases, anchors,
|
|
tags, merge keys, encoded or multiline action scalars, and quoted/escaped run scalars; YAML
|
|
semantics outside this deliberately partial diagnostic parser remain covered by the primary
|
|
byte lock rather than an overclaim of complete Bash YAML parsing;
|
|
7. require every Gradle-running job to order checkout, the exact wrapper-validation action with
|
|
stable `id: gradle-wrapper-validation`, and every Gradle invocation;
|
|
8. accept the validation step only with its exact canonical name/id/uses fields and no `if`,
|
|
`continue-on-error`, `with`, `env`, timeout, or other weakening field;
|
|
9. finalize every Gradle step, not only the first. A Gradle step may have no condition or exactly
|
|
`${{ always() && steps.gradle-wrapper-validation.outcome == 'success' }}`; bare `always()`,
|
|
failure/cancelled paths, `continue-on-error`, and other reachability expressions fail closed;
|
|
10. treat literal run-block body text only as shell data, never as an action field, and require each
|
|
raw Gradle reference admitted by the gate to resolve to a canonical job;
|
|
11. print `gradle-wrapper-contract: PASS` only when every check succeeds.
|
|
```
|
|
|
|
For an intentional workflow edit, review the complete workflow diff, verify that no workflow path
|
|
is a symlink/special file, regenerate the entire sorted `sha256sum` list with:
|
|
|
|
```bash
|
|
find .github/workflows -mindepth 1 -maxdepth 1 \
|
|
\( -name '*.yml' -o -name '*.yaml' \) ! -type f -print # must print nothing
|
|
find .github/workflows -mindepth 1 -maxdepth 1 -type f \
|
|
\( -name '*.yml' -o -name '*.yaml' \) -print0 \
|
|
| LC_ALL=C sort -z | xargs -0 sha256sum
|
|
```
|
|
|
|
Replace the complete sorted embedded array in the same reviewed change. Never refresh only the
|
|
failing digest as a build-unblock shortcut.
|
|
|
|
- [ ] **Step 4: Regenerate the wrapper twice and add the distribution checksum**
|
|
|
|
Run in `src/`:
|
|
|
|
```bash
|
|
./gradlew wrapper --gradle-version 9.0.0 --distribution-type bin
|
|
./gradlew wrapper --gradle-version 9.0.0 --distribution-type bin
|
|
```
|
|
|
|
Then add the exact `distributionSha256Sum` property immediately after `distributionUrl`.
|
|
|
|
- [ ] **Step 5: Add the pinned validation action to every Gradle workflow job**
|
|
|
|
After each checkout step and before setup/cache/build invokes Gradle, add:
|
|
|
|
```yaml
|
|
- name: Validate Gradle wrapper
|
|
id: gradle-wrapper-validation
|
|
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
|
```
|
|
|
|
Jobs without a Gradle invocation do not need the action. A sanitizer that intentionally executes
|
|
after a failed test must use the exact guarded condition shown above so wrapper-validation failure
|
|
still prevents Gradle. Preserve that behavior in Redis rather than using bare `always()`.
|
|
|
|
- [ ] **Step 6: Verify GREEN and mutation rejection**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
bash .github/scripts/verify-gradle-wrapper.sh .
|
|
cd src
|
|
./gradlew :app-bootstrap:test --tests '*DeveloperExperienceContractTest' --console=plain
|
|
```
|
|
|
|
Expected: script prints `gradle-wrapper-contract: PASS`; focused tests pass; executable mutations
|
|
reject checksum/property overrides, missing validation per job, named/anonymous/quoted/escaped and
|
|
continued action variants, encoded run scalars, block/alias/merge/flow YAML forms, validation-step
|
|
control fields, Gradle steps reachable after validation failure, custom-shell or alternate-wrapper
|
|
paths, duplicate encoded jobs, workflow additions/removals/symlinks, and otherwise innocuous byte
|
|
drift through the primary workflow lock.
|
|
|
|
- [ ] **Step 7: Record diff evidence without committing**
|
|
|
|
Run `sha256sum src/gradle/wrapper/gradle-wrapper.jar`, `git diff --check`, and `git status --short`.
|
|
|
|
### Task 3: Make Docker Build Configuration Inputs Explicit
|
|
|
|
**Files:**
|
|
- Modify: `src/Dockerfile:39-66`
|
|
- Modify: `src/Dockerfile.sample:50-75`
|
|
- Modify: `src/build.gradle:2153-2181` and all Redis evidence consumers
|
|
- Modify: `src/adapter/outbound/cache-redis/build.gradle` (leaf evidence consumers)
|
|
- Modify: `src/app-bootstrap/build.gradle`
|
|
- Modify: `src/sample-portfolio/build.gradle`
|
|
- Modify: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/DeveloperExperienceContractTest.java`
|
|
|
|
**Interfaces:**
|
|
- Consumes: `config/**`, Gradle source/build files, `-PgitRevision`, and the `bootJar` archive provider.
|
|
- Produces: `:app-bootstrap:stageDockerJar` and `:sample-portfolio:stageDockerJar`, each writing exactly `build/docker/application.jar`; evidence metadata is resolved only when a Redis evidence task executes.
|
|
|
|
- [ ] **Step 1: Write failing build-contract tests**
|
|
|
|
Add tests that split each Dockerfile at its first `RUN ./gradlew` and assert the preceding section
|
|
uses repository-preserving `WORKDIR /build/src` and contains `COPY config/ ./config/`. Add tests
|
|
that require the Dockerfiles to run `stageDockerJar` and copy the exact
|
|
`build/docker/application.jar`, with no `ls | grep | head` selection. Add a test that runs
|
|
`./gradlew help -PgitRevision=0123456789abcdef0123456789abcdef01234567` from a temporary Git-less
|
|
copy containing the same files as the dependency-cache stage. Add three self-contained evidence-task
|
|
fixtures under temporary repository roots: one uses a `.git` directory, one uses a worktree `.git`
|
|
metadata file, and one uses a dangling `.git` symlink. All prepend a fake `git` to `PATH` and require
|
|
the exact named failure for `rev-parse` or `status` process errors; the symlink fixture must also prove
|
|
the link entry exists with `NOFOLLOW_LINKS`. These tests must copy the minimum build/registry inputs
|
|
and invoke the fixture wrapper; they must not assert or execute the ambient checkout's `.git`.
|
|
|
|
- [ ] **Step 2: Verify RED**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cd src
|
|
./gradlew :app-bootstrap:test --tests '*DeveloperExperienceContractTest' --console=plain
|
|
```
|
|
|
|
Expected: FAIL because neither cache stage copies `config/**`, both select JARs with shell matching, and Git is resolved during configuration.
|
|
|
|
- [ ] **Step 3: Add deterministic Docker staging tasks**
|
|
|
|
In both executable modules register:
|
|
|
|
```groovy
|
|
tasks.register('stageDockerJar', Sync) {
|
|
dependsOn tasks.named('bootJar')
|
|
from(tasks.named('bootJar').flatMap { it.archiveFile })
|
|
into(layout.buildDirectory.dir('docker'))
|
|
rename { 'application.jar' }
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Update both Dockerfiles**
|
|
|
|
Use `WORKDIR /build/src` so repository-relative registry paths resolve under `/build/src/**`, copy
|
|
`config/` before the first Gradle invocation, invoke the correct `stageDockerJar` task with the
|
|
existing release/revision properties, and copy only the fixed `build/docker/application.jar` path
|
|
into the runtime stage.
|
|
|
|
- [ ] **Step 5: Move Redis Git evidence resolution to execution time**
|
|
|
|
Replace the eager `String` values with closures/providers invoked from evidence task actions:
|
|
|
|
```groovy
|
|
Closure<Map<String, String>> resolveRedisSourceEvidence = {
|
|
File gitMetadata = rootProject.file('../.git')
|
|
if (!java.nio.file.Files.exists(
|
|
gitMetadata.toPath(), java.nio.file.LinkOption.NOFOLLOW_LINKS)) {
|
|
String attested = providers.gradleProperty('gitRevision')
|
|
.orElse(providers.environmentVariable('GITHUB_SHA'))
|
|
.orElse(providers.environmentVariable('GIT_SHA'))
|
|
.getOrElse('')
|
|
if (!(attested ==~ /[0-9a-f]{40}/)) {
|
|
throw new GradleException(
|
|
'Redis evidence requires an exact 40-character source revision.')
|
|
}
|
|
return [revision: attested, treeState: 'ATTESTED']
|
|
}
|
|
|
|
String headFailure = 'Redis evidence failed to resolve checked-out Git HEAD.'
|
|
def headExecution
|
|
try {
|
|
headExecution = providers.exec {
|
|
commandLine 'git', 'rev-parse', 'HEAD'
|
|
ignoreExitValue = true
|
|
}
|
|
if (headExecution.result.get().exitValue != 0) {
|
|
throw new GradleException(headFailure)
|
|
}
|
|
} catch (GradleException exception) {
|
|
if (exception.message == headFailure) {
|
|
throw exception
|
|
}
|
|
throw new GradleException(headFailure, exception)
|
|
}
|
|
String checkedOut = headExecution.standardOutput.asText.getOrElse('').trim()
|
|
if (!(checkedOut ==~ /[0-9a-f]{40}/)) {
|
|
throw new GradleException(headFailure)
|
|
}
|
|
String supplied = providers.gradleProperty('gitRevision')
|
|
.orElse(providers.environmentVariable('GITHUB_SHA'))
|
|
.orElse(providers.environmentVariable('GIT_SHA'))
|
|
.orElse(checkedOut)
|
|
.getOrElse('')
|
|
if (!(supplied ==~ /[0-9a-f]{40}/)) {
|
|
throw new GradleException('Redis evidence requires an exact 40-character source revision.')
|
|
}
|
|
if (!checkedOut.isBlank() && supplied != checkedOut) {
|
|
throw new GradleException('Redis evidence source revision does not match checked-out HEAD.')
|
|
}
|
|
|
|
String statusFailure = 'Redis evidence failed to inspect checked-out Git status.'
|
|
def statusExecution
|
|
try {
|
|
statusExecution = providers.exec {
|
|
commandLine 'git', 'status', '--porcelain', '--untracked-files=normal'
|
|
ignoreExitValue = true
|
|
}
|
|
if (statusExecution.result.get().exitValue != 0) {
|
|
throw new GradleException(statusFailure)
|
|
}
|
|
} catch (GradleException exception) {
|
|
if (exception.message == statusFailure) {
|
|
throw exception
|
|
}
|
|
throw new GradleException(statusFailure, exception)
|
|
}
|
|
String treeState = statusExecution.standardOutput.asText.getOrElse('').isBlank()
|
|
? 'CLEAN'
|
|
: 'DIRTY'
|
|
[revision: supplied, treeState: treeState]
|
|
}
|
|
```
|
|
|
|
Each evidence-producing root `doLast` and each leaf evidence test's root-suite `afterSuite` resolves
|
|
this once and uses the returned values for all generated/validated artifacts. The resolver is
|
|
exposed as `rootProject.ext.resolveRedisSourceEvidence`; eager scalar ext properties are removed.
|
|
Non-evidence tasks never call the closure. Any repository-root `.git` filesystem entry is detected
|
|
without following symbolic links, so a directory, worktree metadata file, or dangling symlink always
|
|
selects the checkout branch. Both Git processes must start, exit zero, and return valid evidence
|
|
before `CLEAN` or `DIRTY` can be emitted. `ATTESTED` is reserved for a truly absent `.git` entry in
|
|
an explicitly Git-less build with an exact supplied revision; a Git execution failure must never
|
|
fall back to it.
|
|
|
|
- [ ] **Step 6: Verify GREEN without `.git` and verify evidence mismatch failure**
|
|
|
|
Run the focused contract test, `./gradlew help` in the Git-less fixture with a 40-character
|
|
`gitRevision`, and one Redis evidence task in the real checkout. The Git-less help invocation must
|
|
pass; a Git-less Redis evidence task with a short revision must fail with the named message. Separate
|
|
self-contained fixtures must cover a `.git` directory whose `rev-parse` fails, a `.git` worktree file
|
|
whose `status` fails, and a dangling `.git` symlink whose Git invocation fails. Each fixture must
|
|
assert the corresponding named fail-closed diagnostic instead of accepting a generic non-zero exit.
|
|
|
|
- [ ] **Step 7: Run actual Docker smoke when Docker is available**
|
|
|
|
Run both image builds with `--no-cache`. If Docker is unavailable, record the exact blocker and leave these commands as remaining risk; do not claim Docker success from string tests.
|
|
|
|
- [ ] **Step 8: Record diff evidence without committing**
|
|
|
|
Run `git diff --check` and `git status --short`.
|
|
|
|
### Task 4: Complete SpotBugs Auxiliary Classpaths and Remove the Gradle 10 Warning
|
|
|
|
**Files:**
|
|
- Modify: `src/build.gradle:208-360`
|
|
- Modify: `src/build.gradle:1760-1795`
|
|
- Test/verify: app-bootstrap redisComposition, inbound GraphQL main, inbound gRPC main SpotBugs tasks
|
|
|
|
**Interfaces:**
|
|
- Consumes: every leaf's `SourceSetContainer` and the SpotBugs task named for each source set.
|
|
- Produces: each SpotBugs task's `auxClassPaths` containing `sourceSet.runtimeClasspath - sourceSet.output` and a required XML report whose analysis errors/missing classes are checked after execution; `verifyApplicationCoreDependencyPurity` uses a configuration-time `Project` reference and declares its execution-time configuration traversal incompatible with the configuration cache.
|
|
|
|
- [ ] **Step 1: Capture the failing static-analysis evidence**
|
|
|
|
Run clean focused SpotBugs tasks and save output. Expected RED messages name Spring Session, `io.micrometer.context.ContextSnapshot`, and protobuf types as classes needed for analysis.
|
|
|
|
- [ ] **Step 2: Capture the Gradle 10 deprecation RED**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cd src
|
|
./gradlew verifyApplicationCoreDependencyPurity --warning-mode=fail --console=plain
|
|
```
|
|
|
|
Expected: FAIL on execution-time `Task.project` access.
|
|
|
|
- [ ] **Step 3: Configure source-set-derived auxiliary classpaths**
|
|
|
|
After applying SpotBugs in each leaf, configure:
|
|
|
|
```groovy
|
|
sourceSets.configureEach { sourceSet ->
|
|
String taskName = "spotbugs${sourceSet.name.capitalize()}"
|
|
tasks.named(taskName, com.github.spotbugs.snom.SpotBugsTask) {
|
|
auxClassPaths.from(sourceSet.runtimeClasspath - sourceSet.output)
|
|
def xmlAnalysisReport = reports.maybeCreate('xml')
|
|
xmlAnalysisReport.required.set(true)
|
|
doLast {
|
|
List<String> analysisFailures =
|
|
spotBugsAnalysisFailures(xmlAnalysisReport.outputLocation.get().asFile)
|
|
if (!analysisFailures.isEmpty()) {
|
|
throw new GradleException(
|
|
"${path}: SpotBugs analysis incomplete:\n " +
|
|
analysisFailures.join('\n '))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Do not add compile/runtime dependencies solely for SpotBugs. The XML parser fails on a missing or
|
|
malformed report, malformed `Errors` counts, any `MissingClass`, and any analysis `Error`; ordinary
|
|
`BugInstance` findings remain governed by the existing main/test severity policy. Wire an
|
|
executable `verifySpotBugsAnalysisFailureContract` fixture into every leaf `check` so clean and
|
|
advisory-bug-only reports pass while missing-class and analysis-error reports fail.
|
|
|
|
- [ ] **Step 4: Remove execution-time project access**
|
|
|
|
Resolve `Project applicationCoreProject = project(':application-core')` before registering
|
|
`verifyApplicationCoreDependencyPurity`; capture that variable in `doLast` instead of calling
|
|
`project(...)` from the task action. Because the action still traverses project configurations at
|
|
execution time, declare
|
|
`notCompatibleWithConfigurationCache('Inspects project configurations at execution time')` rather
|
|
than making an unsupported compatibility claim.
|
|
|
|
- [ ] **Step 5: Verify GREEN**
|
|
|
|
Run `verifySpotBugsAnalysisFailureContract`, the three clean focused SpotBugs tasks, and
|
|
`verifyApplicationCoreDependencyPurity --warning-mode=fail`. Expected: exit 0, XML
|
|
`Errors errors="0" missingClasses="0"`, and no missing-analysis-class/deprecation output.
|
|
|
|
- [ ] **Step 6: Run release-hygiene aggregate verification**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cd src
|
|
./gradlew clean check :app-bootstrap:sampleOffTest verifyPublicPathSnapshot verifyDependencyLocks --no-daemon --console=plain --warning-mode=fail
|
|
cd ..
|
|
bash .github/scripts/verify-gate-matrix.sh
|
|
bash .github/scripts/verify-gradle-wrapper.sh .
|
|
```
|
|
|
|
Expected: every command exits 0; no skipped mandatory gate, missing SpotBugs class, or Gradle deprecation.
|
|
|
|
- [ ] **Step 7: Record final diff evidence without committing**
|
|
|
|
Run `git diff --check`, `git diff --stat`, and `git status --short`. Dispatch the complete diff for architecture/spec and code-quality review.
|
|
|
|
## Plan Self-Review
|
|
|
|
- Spec coverage: every release-hygiene design decision maps to Tasks 1-4.
|
|
- Type consistency: both executable modules expose the same `stageDockerJar` task and output path; Redis evidence uses one `Map<String,String>` resolver contract.
|
|
- Architecture: no production dependency edge changes are required.
|
|
- Test discipline: each behavior has a named failing command or executable mutation fixture before implementation.
|
|
- Commit policy: all generic commit steps are replaced with diff/status evidence.
|