mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-23 12:18:11 +00:00
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>
230 lines
7.8 KiB
YAML
230 lines
7.8 KiB
YAML
name: iOS Periphery Dead Code
|
|
|
|
on:
|
|
pull_request:
|
|
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
|
|
workflow_dispatch:
|
|
|
|
concurrency:
|
|
group: ios-periphery-${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
|
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
|
|
jobs:
|
|
scope:
|
|
name: Detect iOS scan scope
|
|
runs-on: ubuntu-24.04
|
|
outputs:
|
|
should-scan: ${{ steps.scope.outputs.should-scan }}
|
|
steps:
|
|
- name: Detect changed paths
|
|
id: scope
|
|
uses: actions/github-script@v9
|
|
with:
|
|
script: |
|
|
if (context.eventName === "workflow_dispatch") {
|
|
core.setOutput("should-scan", "true");
|
|
return;
|
|
}
|
|
if (context.payload.pull_request?.draft) {
|
|
core.setOutput("should-scan", "false");
|
|
return;
|
|
}
|
|
|
|
const files = await github.paginate(github.rest.pulls.listFiles, {
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: context.payload.pull_request.number,
|
|
per_page: 100,
|
|
});
|
|
const isScanPath = (filename) =>
|
|
typeof filename === "string" && (
|
|
filename.startsWith("apps/ios/") ||
|
|
filename === ".github/workflows/ios-periphery.yml" ||
|
|
filename === ".github/workflows/ios-periphery-comment.yml" ||
|
|
filename === "config/swiftformat" ||
|
|
filename === "config/swiftlint.yml"
|
|
);
|
|
const shouldScan = files.some(
|
|
({ filename, previous_filename: previousFilename }) =>
|
|
isScanPath(filename) || isScanPath(previousFilename)
|
|
);
|
|
core.setOutput("should-scan", String(shouldScan));
|
|
|
|
scan:
|
|
name: Scan iOS dead code
|
|
needs: scope
|
|
if: ${{ needs.scope.outputs.should-scan == 'true' }}
|
|
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'macos-26' || (github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-26' || 'macos-26') }}
|
|
timeout-minutes: 45
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 1
|
|
fetch-tags: false
|
|
persist-credentials: false
|
|
submodules: false
|
|
|
|
- name: Verify Xcode
|
|
run: |
|
|
set -euo pipefail
|
|
for xcode_app in /Applications/Xcode_26.5.app /Applications/Xcode-26.5.0.app; do
|
|
if [ -d "$xcode_app/Contents/Developer" ]; then
|
|
sudo xcode-select -s "$xcode_app/Contents/Developer"
|
|
break
|
|
fi
|
|
done
|
|
xcodebuild -version
|
|
xcode_version="$(xcodebuild -version | awk 'NR == 1 { print $2 }')"
|
|
if [[ "$xcode_version" != 26.* ]]; then
|
|
echo "error: expected Xcode 26.x, got $xcode_version" >&2
|
|
exit 1
|
|
fi
|
|
swift --version
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
|
|
- name: Install iOS Swift tooling
|
|
run: brew install xcodegen swiftformat swiftlint periphery
|
|
|
|
- name: Generate iOS project
|
|
run: |
|
|
set -euo pipefail
|
|
./scripts/ios-configure-signing.sh
|
|
./scripts/ios-write-version-xcconfig.sh
|
|
cd apps/ios
|
|
xcodegen generate
|
|
|
|
- name: Run Periphery
|
|
run: |
|
|
set -euo pipefail
|
|
output_dir="$RUNNER_TEMP/ios-periphery"
|
|
mkdir -p "$output_dir"
|
|
cd apps/ios
|
|
set +e
|
|
periphery scan \
|
|
--config .periphery.yml \
|
|
--strict \
|
|
--format json \
|
|
--write-results "$output_dir/periphery.json" \
|
|
>"$output_dir/periphery.stdout.json" \
|
|
2>"$output_dir/periphery.stderr.log"
|
|
periphery_status="$?"
|
|
set -e
|
|
printf '%s\n' "$periphery_status" >"$output_dir/periphery.status"
|
|
if [ ! -s "$output_dir/periphery.json" ]; then
|
|
cp "$output_dir/periphery.stdout.json" "$output_dir/periphery.json"
|
|
fi
|
|
|
|
- name: Build Periphery report
|
|
run: |
|
|
set -euo pipefail
|
|
node <<'NODE'
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const outputDir = path.join(process.env.RUNNER_TEMP, "ios-periphery");
|
|
const read = (name) => {
|
|
const file = path.join(outputDir, name);
|
|
return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
};
|
|
|
|
const status = Number(read("periphery.status").trim() || "1");
|
|
let findings = null;
|
|
for (const name of ["periphery.json", "periphery.stdout.json"]) {
|
|
try {
|
|
const parsed = JSON.parse(read(name));
|
|
if (Array.isArray(parsed)) {
|
|
findings = parsed;
|
|
break;
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
const escapeCommandData = (value) =>
|
|
String(value ?? "")
|
|
.replaceAll("%", "%25")
|
|
.replaceAll("\r", "%0D")
|
|
.replaceAll("\n", "%0A");
|
|
const escapeCommandProperty = (value) =>
|
|
escapeCommandData(value)
|
|
.replaceAll(":", "%3A")
|
|
.replaceAll(",", "%2C");
|
|
|
|
const rows = (findings ?? []).map((finding) => {
|
|
const location = String(finding.location ?? "");
|
|
const [file, line] = location.split(":");
|
|
const repoFile = file ? `apps/ios/${file}` : "";
|
|
return {
|
|
file: repoFile,
|
|
line: line || "",
|
|
kind: String(finding.kind ?? ""),
|
|
name: String(finding.name ?? ""),
|
|
};
|
|
});
|
|
|
|
for (const row of rows) {
|
|
if (!row.file) continue;
|
|
const line = row.line ? `,line=${escapeCommandProperty(row.line)}` : "";
|
|
const title = `${row.kind || "Unused code"} ${row.name}`.trim();
|
|
console.log(`::error file=${escapeCommandProperty(row.file)}${line},title=Dead Swift code::${escapeCommandData(title)}`);
|
|
}
|
|
|
|
let shouldFail = "1";
|
|
let summary = "";
|
|
|
|
if (findings === null) {
|
|
summary = [
|
|
"### iOS Periphery",
|
|
"",
|
|
"Periphery did not complete. Check the workflow artifact for stdout/stderr.",
|
|
].join("\n");
|
|
} else if (rows.length === 0 && status === 0) {
|
|
shouldFail = "0";
|
|
summary = [
|
|
"### iOS Periphery",
|
|
"",
|
|
"No dead Swift code found.",
|
|
].join("\n");
|
|
} else if (rows.length > 0) {
|
|
summary = [
|
|
"### iOS Periphery",
|
|
"",
|
|
`Found ${rows.length} dead Swift code ${rows.length === 1 ? "symbol" : "symbols"}. See the PR comment or workflow artifact for details.`,
|
|
].join("\n");
|
|
} else {
|
|
summary = [
|
|
"### iOS Periphery",
|
|
"",
|
|
"Periphery exited with a non-zero status before producing findings. Check the workflow artifact for stdout/stderr.",
|
|
].join("\n");
|
|
}
|
|
|
|
fs.writeFileSync(path.join(outputDir, "should-fail.txt"), `${shouldFail}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${summary.trim()}\n`);
|
|
NODE
|
|
|
|
- name: Upload Periphery report
|
|
if: always()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: ios-periphery-dead-code-${{ github.run_id }}-${{ github.run_attempt }}
|
|
path: ${{ runner.temp }}/ios-periphery
|
|
if-no-files-found: warn
|
|
retention-days: 14
|
|
|
|
- name: Fail on dead code
|
|
run: |
|
|
set -euo pipefail
|
|
test "$(cat "$RUNNER_TEMP/ios-periphery/should-fail.txt")" = "0"
|