




















1+name: iOS Periphery Dead Code
2+3+on:
4+pull_request:
5+types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
6+workflow_dispatch:
7+8+concurrency:
9+group: ios-periphery-${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
10+cancel-in-progress: true
11+12+env:
13+FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
14+15+permissions:
16+contents: read
17+pull-requests: read
18+19+jobs:
20+scope:
21+name: Detect iOS scan scope
22+runs-on: ubuntu-24.04
23+outputs:
24+should-scan: ${{ steps.scope.outputs.should-scan }}
25+steps:
26+ - name: Detect changed paths
27+id: scope
28+uses: actions/github-script@v9
29+with:
30+script: |
31+ if (context.eventName === "workflow_dispatch") {
32+ core.setOutput("should-scan", "true");
33+ return;
34+ }
35+ if (context.payload.pull_request?.draft) {
36+ core.setOutput("should-scan", "false");
37+ return;
38+ }
39+40+ const files = await github.paginate(github.rest.pulls.listFiles, {
41+ owner: context.repo.owner,
42+ repo: context.repo.repo,
43+ pull_number: context.payload.pull_request.number,
44+ per_page: 100,
45+ });
46+ const isScanPath = (filename) =>
47+ typeof filename === "string" && (
48+ filename.startsWith("apps/ios/") ||
49+ filename === ".github/workflows/ios-periphery.yml" ||
50+ filename === ".github/workflows/ios-periphery-comment.yml" ||
51+ filename === "config/swiftformat" ||
52+ filename === "config/swiftlint.yml"
53+ );
54+ const shouldScan = files.some(
55+ ({ filename, previous_filename: previousFilename }) =>
56+ isScanPath(filename) || isScanPath(previousFilename)
57+ );
58+ core.setOutput("should-scan", String(shouldScan));
59+60+ scan:
61+name: Scan iOS dead code
62+needs: scope
63+if: ${{ needs.scope.outputs.should-scan == 'true' }}
64+runs-on: ${{ github.event_name == 'workflow_dispatch' && 'macos-26' || (github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-26' || 'macos-26') }}
65+timeout-minutes: 45
66+steps:
67+ - name: Checkout
68+uses: actions/checkout@v6
69+with:
70+fetch-depth: 1
71+fetch-tags: false
72+persist-credentials: false
73+submodules: false
74+75+ - name: Verify Xcode
76+run: |
77+ set -euo pipefail
78+ for xcode_app in /Applications/Xcode_26.5.app /Applications/Xcode-26.5.0.app; do
79+ if [ -d "$xcode_app/Contents/Developer" ]; then
80+ sudo xcode-select -s "$xcode_app/Contents/Developer"
81+ break
82+ fi
83+ done
84+ xcodebuild -version
85+ xcode_version="$(xcodebuild -version | awk 'NR == 1 { print $2 }')"
86+ if [[ "$xcode_version" != 26.* ]]; then
87+ echo "error: expected Xcode 26.x, got $xcode_version" >&2
88+ exit 1
89+ fi
90+ swift --version
91+92+ - name: Setup Node environment
93+uses: ./.github/actions/setup-node-env
94+with:
95+install-bun: "false"
96+97+ - name: Install iOS Swift tooling
98+run: brew install xcodegen swiftformat swiftlint periphery
99+100+ - name: Generate iOS project
101+run: |
102+ set -euo pipefail
103+ ./scripts/ios-configure-signing.sh
104+ ./scripts/ios-write-version-xcconfig.sh
105+ cd apps/ios
106+ xcodegen generate
107+108+ - name: Run Periphery
109+run: |
110+ set -euo pipefail
111+ output_dir="$RUNNER_TEMP/ios-periphery"
112+ mkdir -p "$output_dir"
113+ cd apps/ios
114+ set +e
115+ periphery scan \
116+ --config .periphery.yml \
117+ --strict \
118+ --format json \
119+ --write-results "$output_dir/periphery.json" \
120+ >"$output_dir/periphery.stdout.json" \
121+ 2>"$output_dir/periphery.stderr.log"
122+ periphery_status="$?"
123+ set -e
124+ printf '%s\n' "$periphery_status" >"$output_dir/periphery.status"
125+ if [ ! -s "$output_dir/periphery.json" ]; then
126+ cp "$output_dir/periphery.stdout.json" "$output_dir/periphery.json"
127+ fi
128+129+ - name: Build Periphery report
130+run: |
131+ set -euo pipefail
132+ node <<'NODE'
133+ const fs = require("node:fs");
134+ const path = require("node:path");
135+136+ const outputDir = path.join(process.env.RUNNER_TEMP, "ios-periphery");
137+ const read = (name) => {
138+ const file = path.join(outputDir, name);
139+ return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
140+ };
141+142+ const status = Number(read("periphery.status").trim() || "1");
143+ let findings = null;
144+ for (const name of ["periphery.json", "periphery.stdout.json"]) {
145+ try {
146+ const parsed = JSON.parse(read(name));
147+ if (Array.isArray(parsed)) {
148+ findings = parsed;
149+ break;
150+ }
151+ } catch {}
152+ }
153+154+ const escapeCommandData = (value) =>
155+ String(value ?? "")
156+ .replaceAll("%", "%25")
157+ .replaceAll("\r", "%0D")
158+ .replaceAll("\n", "%0A");
159+ const escapeCommandProperty = (value) =>
160+ escapeCommandData(value)
161+ .replaceAll(":", "%3A")
162+ .replaceAll(",", "%2C");
163+164+ const rows = (findings ?? []).map((finding) => {
165+ const location = String(finding.location ?? "");
166+ const [file, line] = location.split(":");
167+ const repoFile = file ? `apps/ios/${file}` : "";
168+ return {
169+ file: repoFile,
170+ line: line || "",
171+ kind: String(finding.kind ?? ""),
172+ name: String(finding.name ?? ""),
173+ };
174+ });
175+176+ for (const row of rows) {
177+ if (!row.file) continue;
178+ const line = row.line ? `,line=${escapeCommandProperty(row.line)}` : "";
179+ const title = `${row.kind || "Unused code"} ${row.name}`.trim();
180+ console.log(`::error file=${escapeCommandProperty(row.file)}${line},title=Dead Swift code::${escapeCommandData(title)}`);
181+ }
182+183+ let shouldFail = "1";
184+ let summary = "";
185+186+ if (findings === null) {
187+ summary = [
188+ "### iOS Periphery",
189+ "",
190+ "Periphery did not complete. Check the workflow artifact for stdout/stderr.",
191+ ].join("\n");
192+ } else if (rows.length === 0 && status === 0) {
193+ shouldFail = "0";
194+ summary = [
195+ "### iOS Periphery",
196+ "",
197+ "No dead Swift code found.",
198+ ].join("\n");
199+ } else if (rows.length > 0) {
200+ summary = [
201+ "### iOS Periphery",
202+ "",
203+ `Found ${rows.length} dead Swift code ${rows.length === 1 ? "symbol" : "symbols"}. See the PR comment or workflow artifact for details.`,
204+ ].join("\n");
205+ } else {
206+ summary = [
207+ "### iOS Periphery",
208+ "",
209+ "Periphery exited with a non-zero status before producing findings. Check the workflow artifact for stdout/stderr.",
210+ ].join("\n");
211+ }
212+213+ fs.writeFileSync(path.join(outputDir, "should-fail.txt"), `${shouldFail}\n`);
214+ fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${summary.trim()}\n`);
215+ NODE
216+217+ - name: Upload Periphery report
218+if: always()
219+uses: actions/upload-artifact@v7
220+with:
221+name: ios-periphery-dead-code-${{ github.run_id }}-${{ github.run_attempt }}
222+path: ${{ runner.temp }}/ios-periphery
223+if-no-files-found: warn
224+retention-days: 14
225+226+ - name: Fail on dead code
227+run: |
228+ set -euo pipefail
229+ test "$(cat "$RUNNER_TEMP/ios-periphery/should-fail.txt")" = "0"
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。