Files
Josh Avant cd2724e432 feat(release): automate iOS and Android store releases (#158807)
Add manually triggered iOS Store Release and Android Store Release workflows with the same local release commands. Derive store versions and OpenAI release notes at build time, retain recovery artifacts, and avoid generated metadata commits.

Pin queued releases to their dispatch commit, qualify native iOS behavior before signing, and improve Android screenshots and interrupted artifact recovery.

Validation includes focused release/planner tests, workflow and type checks, native iOS qualification, Android screenshot capture, and historical release-note exercises. CI failure attribution and remaining hosted release verification are documented in the PR.

Closes #158806
2026-09-26 06:28:29 -05:00

246 lines
8.8 KiB
YAML

name: macOS Periphery Dead Code
# Job conclusions cannot distinguish passive draft events from scope loss.
# Record source admission for the commenter before either job runs.
run-name: ${{ github.workflow }} [${{ github.event_name == 'workflow_dispatch' && 'manual' || github.event.pull_request.draft == false && 'report' || github.event.action == 'converted_to_draft' && 'draft' || 'passive' }}]
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
workflow_dispatch:
concurrency:
# Admission precedes scope skips: isolate passive drafts before they can cancel useful work.
# Keep conversion events in the PR group as an intentional cancellation signal.
group: >-
${{ github.event_name == 'pull_request' && github.event.pull_request.draft && github.event.action != 'converted_to_draft'
&& format('{0}-passive-draft-{1}', github.workflow, github.run_id)
|| format('macos-periphery-{0}-{1}', github.workflow, github.event.pull_request.number || github.sha) }}
cancel-in-progress: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
permissions:
contents: read
jobs:
scope:
name: Detect macOS scan scope
runs-on: ubuntu-24.04
outputs:
should-scan: ${{ steps.scope.outputs.should-scan }}
steps:
- name: Checkout
if: github.event_name == 'pull_request' && github.event.pull_request.draft == false
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 2
fetch-tags: false
persist-credentials: false
submodules: false
- name: Detect changed paths
id: scope
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # 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;
}
// pull_request checks out GitHub's synthetic merge commit. Its first
// parent is the exact base tree used to produce the tested result.
const result = await exec.getExecOutput("git", [
"diff",
"--quiet",
"HEAD^1",
"HEAD",
"--",
"apps/macos/",
".github/workflows/macos-periphery.yml",
".github/workflows/ios-periphery-comment.yml",
"apps/macos/.periphery.yml",
"scripts/install-periphery.sh",
"scripts/lib/swift-toolchain.sh",
], { ignoreReturnCode: true, silent: true });
if (result.exitCode !== 0 && result.exitCode !== 1) {
throw new Error(`git diff failed with exit code ${result.exitCode}`);
}
core.setOutput("should-scan", String(result.exitCode === 1));
scan:
name: Scan macOS dead code
needs: scope
if: ${{ needs.scope.outputs.should-scan == 'true' }}
runs-on: xcode-27
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Verify Xcode
run: |
set -euo pipefail
source scripts/lib/swift-toolchain.sh
select_xcode_toolchain
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
cache-mode: restore
install-bun: "false"
install-deps: "false"
- name: Install Mermaid renderer dependencies
env:
CI: "true"
run: >-
pnpm install --frozen-lockfile --prefer-offline --optional
--filter '@openclaw/mermaid-renderer...'
--config.ignore-scripts=false
--config.engine-strict=false
--config.enable-pre-post-scripts=true
--config.side-effects-cache=true
- name: Prepare Apple Mermaid assets
run: node scripts/prepare-apple-mermaid.mjs
- name: Install Periphery
run: |
periphery_dir="$RUNNER_TEMP/openclaw-periphery"
./scripts/install-periphery.sh "$periphery_dir"
echo "$periphery_dir" >> "$GITHUB_PATH"
- name: Run Periphery
run: |
set -euo pipefail
output_dir="$RUNNER_TEMP/macos-periphery"
mkdir -p "$output_dir"
cd apps/macos
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, "macos-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/macos/${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 = [
"### macOS Periphery",
"",
"Periphery did not complete. Check the workflow artifact for stdout/stderr.",
].join("\n");
} else if (rows.length === 0 && status === 0) {
shouldFail = "0";
summary = [
"### macOS Periphery",
"",
"No dead Swift code found.",
].join("\n");
} else if (rows.length > 0) {
summary = [
"### macOS 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 = [
"### macOS 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: macos-periphery-dead-code-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/macos-periphery
if-no-files-found: error
retention-days: 14
- name: Fail on dead code
run: |
set -euo pipefail
test "$(cat "$RUNNER_TEMP/macos-periphery/should-fail.txt")" = "0"