-
Notifications
You must be signed in to change notification settings - Fork 0
259 lines (253 loc) · 12.5 KB
/
Copy path_java.yml
File metadata and controls
259 lines (253 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# Reusable Java / Gradle gate.
#
# jobs:
# gate:
# uses: CMaintz/foundry/.github/workflows/_java.yml@<sha>
# with:
# spotbugs: true # opt-in bytecode analysis
#
# no-`var` is not an input: it's folded into `lint` (mise.toml), enforced by the gate.
#
# Runs the same six verbs a developer runs locally (`mise run gate`). If this and
# a laptop ever disagree, that's a bug in the setup, not the code. Toolchain (JDK)
# comes from the repo's mise.toml; Gradle caching from setup-gradle.
#
# Input names are snake_case, not kebab: `${{ inputs.mise-version }}` parses as
# the subtraction `inputs.mise - version` and fails the workflow at startup.
name: java
on:
workflow_call:
inputs:
mise_version:
type: string
default: "2026.9.2"
working_directory:
description: Directory containing mise.toml, gradlew and .habit-hooks/.
type: string
default: "."
habit_hooks:
description: Run the structural-smell sensors (PMD). Needs a .habit-hooks/ dir.
type: boolean
default: true
habit_hooks_plugin:
type: string
default: "habit-hooks-java"
spotbugs:
description: >-
Run SpotBugs (opt-in). Invokes the `spotbugs` mise task, so the consumer
decides the exact Gradle call — the java template defaults it to
`./gradlew spotbugsMain`; a root-Gradle monorepo overrides it (e.g.
`../gradlew :backend:spotbugsMain`). Auxiliary, not a contract verb.
type: boolean
default: false
osv_scanner_version:
type: string
default: "2.5.1"
pmd_version:
type: string
default: "7.27.0"
permissions:
contents: read
jobs:
gate:
name: Deterministic gate
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ inputs.working_directory }}
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
fetch-depth: 0 # Spotless ratchetFrom origin/main needs history, not a shallow tip
# If the repo pins PMD via the mise.toml postinstall hook, `mise install` here
# would re-download it every run even though the gate doesn't use PMD. Restore
# it first so setup:pmd no-ops on a hit (harmless empty cache if PMD isn't pinned).
- name: Cache PMD
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4
with:
path: ~/.local/opt/pmd-bin-${{ inputs.pmd_version }}
key: pmd-${{ inputs.pmd_version }}
- uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3
with:
version: ${{ inputs.mise_version }}
cache: true
- uses: gradle/actions/setup-gradle@94bac82a5b62952e304b4f7a45a90e19c46c7e50 # v4
# `mise run audit` shells out to osv-scanner over a generated Gradle lockfile.
- name: Install osv-scanner
run: |
mkdir -p "$HOME/.local/bin"
curl -fsSL -o "$HOME/.local/bin/osv-scanner" \
"https://github.com/google/osv-scanner/releases/download/v${{ inputs.osv_scanner_version }}/osv-scanner_linux_amd64"
chmod +x "$HOME/.local/bin/osv-scanner"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
# This IS `mise run gate` (lint -> typecheck -> test -> audit, in order),
# split one verb per step so a failure reddens the exact verb in the UI and
# gets a targeted fix. Devs still run the composite `mise run gate` locally.
- name: 'gate: lint'
id: lint
# FOUNDRY_BASE_REF is the ratchet base for lint's no-`var` check (see mise.toml):
# the PR base SHA on PRs (origin/main isn't a reliable ref in a PR checkout),
# empty on dispatch ⇒ novar falls back to origin/main. fetch-depth: 0 (above)
# supplies the history the merge-base needs.
env:
FOUNDRY_BASE_REF: ${{ github.event.pull_request.base.sha }}
run: mise run lint
- name: How to fix (lint)
if: failure() && steps.lint.outcome == 'failure'
run: |
{
echo "## ❌ Gate failed at \`lint\` — formatting (Spotless) or no-\`var\`"
echo ""
echo "**Formatting** is 100% mechanical — run it, commit, push:"
echo '```'
echo "mise run fix # = ./gradlew spotlessApply"
echo '```'
echo "**no-\`var\`** is not auto-fixable: replace \`var\` with the explicit type, or tag a justified use \`// foundry-allow-var: reason\`. It only flags \`var\` on files this PR changed."
echo "(Structural smells are a separate 'Structural smells' job, not this one.) The offending files are in the **gate: lint** step log above."
} >> "$GITHUB_STEP_SUMMARY"
- name: 'gate: typecheck'
id: typecheck
run: mise run typecheck
- name: How to fix (typecheck)
if: failure() && steps.typecheck.outcome == 'failure'
run: |
{
echo "## ❌ Gate failed at \`typecheck\` — compilation"
echo ""
echo "Not auto-fixable. The file:line is in the **gate: typecheck** step log above; reproduce with \`mise run typecheck\`."
} >> "$GITHUB_STEP_SUMMARY"
- name: 'gate: test'
id: test
run: mise run test
- name: How to fix (test)
if: failure() && steps.test.outcome == 'failure'
run: |
{
echo "## ❌ Gate failed at \`test\`"
echo ""
echo "A test failed or coverage fell below the floor. Reproduce with \`mise run test\`; the failing test is in the **gate: test** step log and the uploaded test report artifact."
} >> "$GITHUB_STEP_SUMMARY"
- name: 'gate: audit'
id: audit
run: mise run audit
- name: How to fix (audit)
if: failure() && steps.audit.outcome == 'failure'
run: |
{
echo "## ❌ Gate failed at \`audit\` — vulnerable dependency"
echo ""
echo "Bump the flagged dependency (or a version override) to a patched release. If it's a false positive or unfixable now, accept that **specific** CVE with justification in osv-scanner.toml — never silence the whole check."
} >> "$GITHUB_STEP_SUMMARY"
- name: Publish test report
if: always() # a failing run is exactly when the report is worth having
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: java-test-report
path: ${{ inputs.working_directory }}/build/reports/tests/test
retention-days: 7
if-no-files-found: ignore
habits:
name: Structural smells
if: inputs.habit_hooks
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ inputs.working_directory }}
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
fetch-depth: 0
- name: Baseline present?
id: baseline
run: |
if [ -f .habit-hooks/snooze.json ]; then
echo "have=true" >> "$GITHUB_OUTPUT"
else
echo "::notice::No .habit-hooks/snooze.json yet — run bootstrap.yml once. Skipping."
echo "have=false" >> "$GITHUB_OUTPUT"
fi
- uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3
if: steps.baseline.outputs.have == 'true'
with:
version: ${{ inputs.mise_version }}
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5
if: steps.baseline.outputs.have == 'true'
with:
python-version: '3.12'
- name: Cache PMD
if: steps.baseline.outputs.have == 'true'
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4
with:
path: ~/.local/opt/pmd-bin-${{ inputs.pmd_version }}
key: pmd-${{ inputs.pmd_version }}
- name: Install PMD + habit-hooks
if: steps.baseline.outputs.have == 'true'
run: |
if [ ! -x "$HOME/.local/opt/pmd-bin-${{ inputs.pmd_version }}/bin/pmd" ]; then
mkdir -p "$HOME/.local/opt"
curl -fsSL -o /tmp/pmd.zip \
"https://github.com/pmd/pmd/releases/download/pmd_releases/${{ inputs.pmd_version }}/pmd-dist-${{ inputs.pmd_version }}-bin.zip"
unzip -q /tmp/pmd.zip -d "$HOME/.local/opt"
fi
echo "$HOME/.local/opt/pmd-bin-${{ inputs.pmd_version }}/bin" >> "$GITHUB_PATH"
pip install --disable-pip-version-check habit-hooks ${{ inputs.habit_hooks_plugin }}
# `--branch` (below) diffs the checked-out branch against `branchBase` (habit-hooks
# default `main`) and scans ONLY what this branch changed — not the whole tree on
# every PR (the full scan is a baseline-time job, see bootstrap.yml). A PR checkout
# is detached at the merge commit, so `main` has no local ref and habit-hooks
# refuses to guess (a hard error, never a silent pass). Point a local `main` at the
# PR base — already in history from fetch-depth: 0. On push, `main` already resolves.
- name: Make base ref resolvable for --branch
if: steps.baseline.outputs.have == 'true' && github.event_name == 'pull_request'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: git branch -f main "$BASE_SHA"
- name: Smells (fails only on smells beyond the snooze baseline)
id: smells
if: steps.baseline.outputs.have == 'true'
# --branch: scan only files changed vs the base. Unchanged files are already
# covered by the snooze baseline; re-scanning them every PR is wasted work.
run: habit-hooks --branch
- name: How to read these smells
if: failure() && steps.smells.outcome == 'failure'
run: |
{
echo "## 📖 Structural smells — what each one means"
echo ""
echo "Each smell is a machine-checkable shadow of a function or file doing **more than one thing**. Fix toward the missing abstraction (a value object, a strategy, a named step) — never by splitting to a line count."
echo ""
echo "| Smell | What it's telling you | Fix toward |"
echo "|---|---|---|"
echo "| \`oversized-function\` | too long to hold one idea | extract a named step / collaborator |"
echo "| \`oversized-file\` | the file carries too many responsibilities | split by concern into cohesive units |"
echo "| \`high-complexity\` | too many branches = too many decisions in one place | replace conditionals with polymorphism/strategy; lift guard clauses |"
echo "| \`too-many-parameters\` | the function juggles too many collaborators | introduce a parameter object, or split the responsibility |"
echo "| \`deep-nesting\` | a nested block wants to be its own named function | extract it; use early returns |"
echo "| \`duplication\` | the same logic lives in two places | extract one shared function |"
echo "| \`dead-code\` / unused export | nothing references it | delete it |"
echo ""
echo "Findings (file + line) are in the **Smells** step log above. Full rationale: \`presets/code-standards.md\`. Clearing a smell is necessary, not sufficient — \"is this *one* thing?\" is still your call."
} >> "$GITHUB_STEP_SUMMARY"
spotbugs:
name: SpotBugs
if: inputs.spotbugs
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ inputs.working_directory }}
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3
with:
version: ${{ inputs.mise_version }}
- uses: gradle/actions/setup-gradle@94bac82a5b62952e304b4f7a45a90e19c46c7e50 # v4
# Runs the consumer's `spotbugs` mise task (java template default:
# `./gradlew spotbugsMain`), so a root-Gradle monorepo can point it at
# `../gradlew :backend:spotbugsMain` without foundry knowing the layout. The
# ratchet (exclude.xml / @SuppressFBWarnings) lives in the consumer's Gradle config.
- run: mise run spotbugs
# no-`var` used to be a separate job here. It's now folded into `lint` (mise.toml),
# so the deterministic gate enforces it in one place — locally and in CI — and it's
# no longer opt-in. The gate job's checkout uses fetch-depth: 0, and it passes
# FOUNDRY_BASE_REF (the PR base SHA) so novar diff-scopes correctly. See
# designs/verb-tiers.md and designs/changed-scope-gate.md.