Skip to content

Commit 233b48d

Browse files
authored
refactor: prune unused iOS code (#91996)
Prune unused iOS surfaces and regenerate the Xcode project. Add a scoped Periphery PR gate with hardened artifact handling and stale-status cleanup. Co-authored-by: Sash Zats <sash@zats.io>
1 parent 501f634 commit 233b48d

44 files changed

Lines changed: 1601 additions & 1363 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ios-periphery-comment.yml

Lines changed: 447 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
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"

apps/ios/.periphery.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
project: OpenClaw.xcodeproj
2+
schemes:
3+
- OpenClaw
4+
retain_codable_properties: true
5+
retain_swift_ui_previews: true
6+
retain_objc_accessible: true
7+
retain_unused_protocol_func_params: true
8+
retain_assign_only_properties: true
9+
relative_results: true
10+
disable_update_check: true
11+
report_include:
12+
- Sources/**
13+
- ShareExtension/**
14+
- ActivityWidget/**
15+
- WatchExtension/Sources/**
16+
build_arguments:
17+
- -destination
18+
- generic/platform=iOS Simulator

apps/ios/Sources/Contacts/ContactsService.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,4 @@ final class ContactsService: ContactsServicing {
202202
phoneNumbers: contact.phoneNumbers.map(\.value.stringValue),
203203
emails: contact.emailAddresses.map { String($0.value) })
204204
}
205-
206-
#if DEBUG
207-
static func _test_matches(contact: CNContact, phoneNumbers: [String], emails: [String]) -> Bool {
208-
self.matchContacts(contacts: [contact], phoneNumbers: phoneNumbers, emails: emails) != nil
209-
}
210-
#endif
211205
}

apps/ios/Sources/Design/AgentProTab+DetailComponents.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import OpenClawKit
2-
import OpenClawProtocol
32
import SwiftUI
43

54
extension AgentProTab {

apps/ios/Sources/Design/AgentProTab.swift

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import OpenClawKit
2-
import OpenClawProtocol
32
import SwiftUI
43

54
struct AgentProTab: View {
65
@Environment(NodeAppModel.self) var appModel
76
@Environment(\.colorScheme) var colorScheme
87
@Environment(\.scenePhase) var scenePhase
9-
let initialRoute: AgentRoute?
108
let directRoute: AgentRoute?
119
let headerLeadingAction: OpenClawSidebarHeaderAction?
1210
let headerTitle: String
@@ -127,13 +125,11 @@ struct AgentProTab: View {
127125
}
128126

129127
init(
130-
initialRoute: AgentRoute? = nil,
131128
directRoute: AgentRoute? = nil,
132129
headerLeadingAction: OpenClawSidebarHeaderAction? = nil,
133130
headerTitle: String = "Agents",
134131
openSettings: (() -> Void)? = nil)
135132
{
136-
self.initialRoute = initialRoute
137133
self.directRoute = directRoute
138134
self.headerLeadingAction = headerLeadingAction
139135
self.headerTitle = headerTitle
@@ -184,9 +180,6 @@ struct AgentProTab: View {
184180
self.destination(for: route)
185181
}
186182
}
187-
.onAppear {
188-
self.applyInitialRouteIfNeeded()
189-
}
190183
}
191184

192185
private func directDestination(for route: AgentRoute) -> some View {
@@ -195,11 +188,4 @@ struct AgentProTab: View {
195188
self.directHeaderLeadingAction(for: route) == nil ? .visible : .hidden,
196189
for: .navigationBar)
197190
}
198-
199-
private func applyInitialRouteIfNeeded() {
200-
guard self.directRoute == nil else { return }
201-
guard let initialRoute else { return }
202-
guard self.navigationPath != [initialRoute] else { return }
203-
self.navigationPath = [initialRoute]
204-
}
205191
}

apps/ios/Sources/Design/CommandCenterSupport.swift

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -185,33 +185,3 @@ struct CommandEmptyStateRow: View {
185185
}
186186
}
187187
}
188-
189-
struct CommandTaskRow: View {
190-
let item: CommandCenterTab.WorkItem
191-
192-
var body: some View {
193-
HStack(alignment: .center, spacing: 6) {
194-
Text(self.item.title)
195-
.font(.footnote.weight(.semibold))
196-
.lineLimit(1)
197-
.minimumScaleFactor(0.80)
198-
.frame(maxWidth: .infinity, minHeight: 20, alignment: .leading)
199-
Text(self.item.detail)
200-
.font(.caption.weight(.medium))
201-
.foregroundStyle(.secondary)
202-
.lineLimit(1)
203-
.minimumScaleFactor(0.78)
204-
.frame(width: 64, alignment: .leading)
205-
if let progress = self.item.progress {
206-
ProProgressBar(progress: progress, color: self.item.color)
207-
.frame(width: 56)
208-
}
209-
Text(self.item.state)
210-
.font(.footnote.weight(.medium))
211-
.foregroundStyle(self.item.progress == nil ? self.item.color : .secondary)
212-
.lineLimit(1)
213-
.frame(width: self.item.progress == nil ? 58 : 34, alignment: .trailing)
214-
}
215-
.padding(.vertical, 8)
216-
}
217-
}

apps/ios/Sources/Design/IPadSkillWorkshopScreen.swift

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -213,32 +213,6 @@ struct IPadSkillWorkshopScreen: View {
213213
}
214214
}
215215

216-
private var statusMenu: some View {
217-
HStack(spacing: 8) {
218-
Text("Status")
219-
.font(.caption.weight(.semibold))
220-
.foregroundStyle(.secondary)
221-
Menu {
222-
ForEach(Self.proposalStatusFilters, id: \.self) { filter in
223-
Button(Self.proposalStatusFilterLabel(filter)) {
224-
self.statusFilter = filter
225-
}
226-
}
227-
} label: {
228-
HStack(spacing: 6) {
229-
Text(self.statusFilterLabel)
230-
.font(.subheadline.weight(.semibold))
231-
Image(systemName: "chevron.up.chevron.down")
232-
.font(.caption2.weight(.bold))
233-
}
234-
.frame(maxWidth: .infinity, alignment: .trailing)
235-
}
236-
.buttonStyle(.bordered)
237-
.controlSize(.small)
238-
.tint(self.neutralControlTint)
239-
}
240-
}
241-
242216
private var agentScopeMenu: some View {
243217
HStack(spacing: 8) {
244218
Text("Agent")
@@ -1130,7 +1104,6 @@ struct IPadSkillProposalRecord: Decodable {
11301104
let description: String
11311105
let createdAt: String
11321106
let updatedAt: String
1133-
let proposedVersion: String
11341107
let target: IPadSkillProposalTarget
11351108
}
11361109

0 commit comments

Comments
 (0)