-
Notifications
You must be signed in to change notification settings - Fork 32
244 lines (220 loc) · 10.7 KB
/
Copy pathci.yml
File metadata and controls
244 lines (220 loc) · 10.7 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
name: CI
on:
push:
branches: [master, dev]
pull_request:
branches: [master, dev]
permissions:
contents: read
jobs:
# Cheap gates that don't need the SDK or Dalamud; run them first so a
# version-sync mistake or a style break fails fast without burning the
# full build.
guards:
name: Guards
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Verify version sync (Directory.Build.props ↔ repo.json)
shell: bash
run: |
build_ver=$(grep -oP '<Version>\K[\d.]+(?=</Version>)' Directory.Build.props)
assembly_ver=$(jq -r '.[0].AssemblyVersion' repo.json)
testing_ver=$(jq -r '.[0].TestingAssemblyVersion' repo.json)
echo "Directory.Build.props <Version> = $build_ver"
echo "repo.json AssemblyVersion = $assembly_ver"
echo "repo.json TestingAssemblyVersion = $testing_ver"
if [ "$build_ver" != "$assembly_ver" ] || [ "$build_ver" != "$testing_ver" ]; then
echo "::error::Version mismatch: Directory.Build.props ($build_ver), repo.json AssemblyVersion ($assembly_ver), TestingAssemblyVersion ($testing_ver) must all match."
exit 1
fi
- name: Verify UI scale seam
shell: bash
run: |
offenders=$(grep -rn 'ImGuiHelpers\.GlobalScale' src --include=*.cs \
| grep -v 'src/Aetherphone/UiScale.cs' || true)
if [ -n "$offenders" ]; then
echo "$offenders"
echo "::error::Draw code must scale through UiScale.Current, not ImGuiHelpers.GlobalScale directly. Only src/Aetherphone/UiScale.cs may read the Dalamud scale."
exit 1
fi
# The dash is built from its UTF-8 bytes so this step does not trip itself,
# and announce-commits.yml is exempt because it carries the character to
# substitute it out of Discord payloads.
- name: Verify no em dashes
shell: bash
run: |
dash=$(printf '\xe2\x80\x94')
offenders=$(git ls-files -z | xargs -0 grep -I -H -n -F -- "$dash" \
| grep -v '^.github/workflows/announce-commits.yml:' || true)
if [ -n "$offenders" ]; then
echo "$offenders"
echo "::error::No em dashes anywhere: not in code, UI strings, locale JSONs, or docs. Use commas, colons, or parentheses."
exit 1
fi
- name: Verify no async void
shell: bash
run: |
offenders=$(grep -rn 'async void' src --include=*.cs || true)
if [ -n "$offenders" ]; then
echo "$offenders"
echo "::error::Zero async void. Return async Task, and fire-and-forget from draw code with an explicit discard: _ = Task.Run(...)."
exit 1
fi
# System.Linq is removed from the implicit usings in Aetherphone.csproj, so
# LINQ cannot be reached without the using line below appearing in a file.
# That makes this list the whole LINQ surface of the plugin rather than a
# sample of operator names, which is what the previous grep checked: it
# named three files while four others were using operators it never listed.
- name: Verify no new LINQ
shell: bash
run: |
offenders=$(grep -rln 'using System\.Linq;' src/Aetherphone --include=*.cs \
| grep -v '^src/Aetherphone/Apps/Calendar/CalendarEvents.cs$' \
| grep -v '^src/Aetherphone/Apps/Music/MusicApp.cs$' \
| grep -v '^src/Aetherphone/Apps/Photos/PhotosApp.Grid.cs$' \
| grep -v '^src/Aetherphone/Apps/Velvet/VelvetFilterSelection.cs$' \
| grep -v '^src/Aetherphone/Apps/Velvet/VelvetNotInterestedArchive.cs$' \
| grep -v '^src/Aetherphone/Apps/Velvet/VelvetStore.cs$' \
| grep -v '^src/Aetherphone/Core/Radio/RadioPlayer.cs$' \
| grep -v '^src/Aetherphone/Core/Songs/SongPlayer.cs$' || true)
if [ -n "$offenders" ]; then
echo "$offenders"
echo "::error::No LINQ in this tree: it allocates iterators and delegates on every call and the whole UI redraws every frame. Write a for loop, or add the file here if the path is genuinely cold."
exit 1
fi
- name: Verify clock format seam
shell: bash
run: |
offenders=$(grep -rnE '"(HH|hh|H|h):mm' src --include=*.cs \
| grep -v 'src/Aetherphone/Core/Localization/TimeText.cs' || true)
if [ -n "$offenders" ]; then
echo "$offenders"
echo "::error::All clock text goes through TimeText.Clock. Only src/Aetherphone/Core/Localization/TimeText.cs may hand-format a time pattern."
exit 1
fi
# Core holds services and domain, Windows holds the shared toolkit, and Apps
# are leaf features that consume both. AppRegistry and WidgetCatalog are the
# two composition roots, so they are the only places allowed to name a leaf.
- name: Verify layering
shell: bash
run: |
offenders=$(grep -rn '^using Aetherphone\.Apps' src/Aetherphone/Core src/Aetherphone/Windows --include=*.cs \
| grep -v '^src/Aetherphone/Core/Apps/AppRegistry.cs:' \
| grep -v '^src/Aetherphone/Windows/Widgets/WidgetCatalog.cs:' || true)
if [ -n "$offenders" ]; then
echo "$offenders"
echo "::error::Core and the Windows toolkit must not reach into an app. Invert onto an interface in Core, or move the shared piece down."
exit 1
fi
# A naming analyzer cannot express this: a private static readonly holding a
# lookup table is PascalCase while one holding mutable state is camelCase,
# so any capitalization rule fires on about a thousand correct names. The
# leading underscore is the part that is unambiguous.
- name: Verify no underscore prefixed fields
shell: bash
run: |
offenders=$(grep -rnE '^[[:space:]]*(private|protected)[[:alnum:][:space:]<>,?\[\]\.]*[[:space:]]_[a-zA-Z]' \
src/Aetherphone src/Aetherphone.Tests --include=*.cs || true)
if [ -n "$offenders" ]; then
echo "$offenders"
echo "::error::Fields carry no leading underscore. Name the field for what it holds instead."
exit 1
fi
# A marker on the first line hides the namespace from the check below, so
# a correct file gets reported as having no namespace at all and the error
# sends you looking in the wrong place. Catch the marker itself and name
# it. .editorconfig asks for utf-8, not utf-8-bom.
- name: Verify no byte order marks
shell: bash
run: |
offenders=$(grep -rlI $'^\xef\xbb\xbf' src/Aetherphone src/Aetherphone.Tests --include=*.cs || true)
if [ -n "$offenders" ]; then
echo "$offenders"
echo "::error::Files carry no byte order mark. Save as utf-8 without one, the way .editorconfig asks."
exit 1
fi
# ManagedDoom is a vendored port and keeps its own layout.
#
# Windows/Components is the one deliberate exception. Its folders group 182
# files by kind so the toolkit is navigable, while every file keeps the flat
# Aetherphone.Windows.Components namespace so consuming it stays one import.
# Sub-namespacing was measured first: it would have added 792 using lines
# across 424 files, and the files that draw the most would have needed five
# to nine imports each. The rule for that subtree is therefore the inverse,
# every file declares exactly the one namespace, and it is checked as such.
- name: Verify namespace matches folder
shell: bash
run: |
toolkit='^src/Aetherphone/Windows/Components/'
offenders=""
while IFS= read -r file; do
actual=$(grep -m1 -oP '^namespace \K[A-Za-z0-9_.]+(?=\s*;)' "$file" || true)
if [[ "$file" =~ $toolkit ]]; then
expected="Aetherphone.Windows.Components"
else
expected=$(dirname "${file#src/}" | tr '/' '.')
fi
if [ -z "$actual" ]; then
offenders+="$file: no file-scoped namespace"$'\n'
elif [ "$actual" != "$expected" ]; then
offenders+="$file: declares $actual, expected $expected"$'\n'
fi
done < <(git ls-files -- src | grep '\.cs$' | grep -v '^src/ManagedDoom/')
if [ -n "$offenders" ]; then
printf '%s' "$offenders"
echo "::error::A file-scoped namespace must match the folder path, except under Windows/Components which is one flat namespace. Move the file or fix the namespace."
exit 1
fi
# Full build. Needs Dalamud present at the standard plugin path so
# Dalamud.NET.Sdk can resolve $(DalamudLibPath).
build:
name: Build (Windows)
needs: guards
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
- name: Download Dalamud
shell: pwsh
run: |
$dalamudUrl = "https://goatcorp.github.io/dalamud-distrib/latest.zip"
$dalamudDir = "$env:AppData\XIVLauncher\addon\Hooks\dev"
New-Item -ItemType Directory -Force -Path $dalamudDir | Out-Null
Invoke-WebRequest -Uri $dalamudUrl -OutFile dalamud.zip
Expand-Archive -Path dalamud.zip -DestinationPath $dalamudDir -Force
Remove-Item dalamud.zip
- name: Restore
run: dotnet restore Aetherphone.sln --locked-mode
- name: Build (Release)
run: dotnet build Aetherphone.sln --configuration Release --no-restore
- name: Run tests
run: >
dotnet test Aetherphone.sln --configuration Release --no-build
--logger "console;verbosity=normal"
--logger "trx;LogFileName=test-results.trx"
--results-directory TestResults
# The console log on a Windows runner is long and the assertion is buried
# in it, so the trx goes up whenever the suite is red.
- name: Upload test results
if: failure()
uses: actions/upload-artifact@v7
with:
name: test-results
path: TestResults/test-results.trx
if-no-files-found: warn
retention-days: 14
# Every pull request gets a loadable build: reviewing this plugin means
# opening the phone in game, so the reviewer needs the zip, not a log.
- name: Upload plugin artifact
uses: actions/upload-artifact@v7
with:
name: Aetherphone-plugin
path: src/Aetherphone/bin/Release/Aetherphone/latest.zip
if-no-files-found: error
retention-days: 14