Repository navigation
191 lines (187 loc) · 8.93 KB
/
Copy path_php.yml
File metadata and controls
191 lines (187 loc) · 8.93 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
# Reusable PHP / Composer gate.
#
# jobs:
# gate:
# uses: CMaintz/foundry/.github/workflows/_php.yml@<sha>
#
# Runs the six verbs a developer runs locally (`mise run gate`). PHP toolchain
# comes from the repo's mise.toml; Composer is preinstalled on the runner.
#
# Input names are snake_case - see the note in ts.yml.
name: php
on:
workflow_call:
inputs:
mise_version:
type: string
default: "2026.9.2"
working_directory:
description: Directory containing mise.toml, composer.json and .habit-hooks/.
type: string
default: "."
habit_hooks:
description: >-
Run the dedicated structural-smell backstop job (phpmd via habit-hooks). NOTE:
`lint` (in the gate job) now also enforces smells whenever
.habit-hooks/config.toml + snooze.json exist; this input only toggles the extra
whole-tree backstop. To opt out entirely, remove the .habit-hooks/ directory.
type: boolean
default: true
habit_hooks_plugin:
type: string
default: "habit-hooks-php"
habit_hooks_version:
description: >-
habit-hooks release to install. The core and its language plugins ship in
lockstep, so the plugin is pinned to the same version. Renovate bumps this.
type: string
default: "1.5.0"
permissions:
contents: read
jobs:
gate:
name: Deterministic gate
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ inputs.working_directory }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0 # `lint` now runs habit-hooks --branch, which diffs vs the PR base
# Python for the habit-hooks venv that mise's setup:habit-hooks postinstall builds -
# BEFORE mise-action so `mise install` can create it. The cache key reads the workflow
# input; the venv PATH in mise.toml reads HABIT_HOOKS_VERSION ([env]) - they default
# equal and are both Renovate-managed, so a mismatch is a cache miss, not a wrong version.
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.12'
- name: Cache habit-hooks
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ~/.local/opt/${{ inputs.habit_hooks_plugin }}-${{ inputs.habit_hooks_version }}
key: ${{ runner.os }}-habit-hooks-${{ inputs.habit_hooks_plugin }}-${{ inputs.habit_hooks_version }}
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4
with:
version: ${{ inputs.mise_version }}
# Cache the vendor tree keyed on composer.lock and skip the install on a hit
# (Composer install is the PHP job's long pole, like npm ci for Node).
- uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
id: vendor
with:
path: ${{ inputs.working_directory }}/vendor
key: vendor-${{ runner.os }}-${{ hashFiles(format('{0}/composer.lock', inputs.working_directory)) }}
- if: steps.vendor.outputs.cache-hit != 'true'
run: composer install --no-interaction --no-progress
# 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 folded structural-smell check
# (habit-hooks --branch): the PR base SHA on PRs, empty on dispatch ⇒ fall back to
# origin/main. The merge-base needs the history that fetch-depth: 0 (above) supplies.
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 / style / smells"
echo ""
echo "Most of this is **mechanically auto-fixable**. Run it, commit, push:"
echo '```'
echo "mise run fix"
echo '```'
echo "Whatever \`fix\` leaves behind is a real style/smell issue - the rule, file and line 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\` - PHPStan"
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 and assertion are in the **gate: test** step log above."
} >> "$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 to a patched version (\`composer update <pkg>\`). If unfixable now, accept that **specific** advisory with justification - never silence the whole check."
} >> "$GITHUB_STEP_SUMMARY"
habits:
name: Structural smells
if: inputs.habit_hooks
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ inputs.working_directory }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
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: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
if: steps.baseline.outputs.have == 'true'
with:
python-version: '3.12'
# phpmd is bundled with the habit-hooks PHP plugin, so this needs only Python.
# (The optional `generic` jscpd plugin additionally needs Node on PATH.)
- name: Install habit-hooks
if: steps.baseline.outputs.have == 'true'
env:
HH_VERSION: ${{ inputs.habit_hooks_version }}
PLUGIN: ${{ inputs.habit_hooks_plugin }}
run: pip install --disable-pip-version-check "habit-hooks==$HH_VERSION" "$PLUGIN==$HH_VERSION"
- name: Smells (fails only on smells beyond the snooze baseline)
id: smells
if: steps.baseline.outputs.have == 'true'
run: habit-hooks
- 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/agent/code-standards.md\`. Clearing a smell is necessary, not sufficient - \"is this *one* thing?\" is still your call."
} >> "$GITHUB_STEP_SUMMARY"