forked from openclaw/openclaw
-
Notifications
You must be signed in to change notification settings - Fork 0
431 lines (403 loc) · 20.2 KB
/
Copy pathandroid-release.yml
File metadata and controls
431 lines (403 loc) · 20.2 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
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
name: Android Release
on:
workflow_dispatch:
inputs:
tag:
description: Existing stable OpenClaw release tag to receive the signed Android APK
required: true
type: string
release_publish_run_id:
description: OpenClaw Release Publish run that approved this release
required: true
type: string
release_publish_branch:
description: Branch used by the approving OpenClaw Release Publish run
required: true
type: string
release_target_sha:
description: Exact release tag commit resolved by OpenClaw Release Publish
required: true
type: string
direct_release_recovery:
description: Allow a completed parent run and published release for explicit backfill or recovery
required: true
default: false
type: boolean
permissions:
actions: read
attestations: write
contents: write
id-token: write
concurrency:
group: android-release-${{ inputs.tag }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.15.0"
jobs:
publish_signed_android_apk:
name: Publish signed Android APK
runs-on: ubuntu-latest
timeout-minutes: 45
environment: android-release
steps:
- name: Checkout release tag
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: refs/tags/${{ inputs.tag }}
fetch-depth: 0
persist-credentials: false
- name: Download parent release approval
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: android-release-approval-${{ inputs.release_publish_run_id }}
path: ${{ runner.temp }}/android-release-approval
repository: ${{ github.repository }}
run-id: ${{ inputs.release_publish_run_id }}
github-token: ${{ github.token }}
- name: Validate release approval and target
id: release_approval
env:
APPROVAL_PATH: ${{ runner.temp }}/android-release-approval/approval.json
DIRECT_RELEASE_RECOVERY: ${{ inputs.direct_release_recovery && 'true' || 'false' }}
EXPECTED_WORKFLOW_BRANCH: ${{ inputs.release_publish_branch }}
GH_TOKEN: ${{ github.token }}
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_TARGET_SHA: ${{ inputs.release_target_sha }}
run: |
set -euo pipefail
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*)?$ ]]; then
echo "Android APK publishing requires a final or correction OpenClaw release tag: ${RELEASE_TAG}" >&2
exit 1
fi
if [[ "${EXPECTED_WORKFLOW_BRANCH}" != "main" && ! "${EXPECTED_WORKFLOW_BRANCH}" =~ ^release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]]; then
echo "release_publish_branch must be main or release/YYYY.M.PATCH." >&2
exit 1
fi
expected_source_ref="refs/tags/${RELEASE_TAG}"
if [[ "${GITHUB_REF}" != "${expected_source_ref}" ]]; then
echo "Android publication must run from ${expected_source_ref}, got ${GITHUB_REF}." >&2
exit 1
fi
if [[ ! "${RELEASE_TARGET_SHA}" =~ ^[a-f0-9]{40}$ ]]; then
echo "release_target_sha must be a full lowercase commit SHA." >&2
exit 1
fi
gh attestation verify "${APPROVAL_PATH}" \
--repo "${GITHUB_REPOSITORY}" \
--signer-workflow "${GITHUB_REPOSITORY}/.github/workflows/openclaw-release-publish.yml" \
--source-ref "refs/heads/${EXPECTED_WORKFLOW_BRANCH}" \
--deny-self-hosted-runners
run_json="$(gh run view "${RELEASE_PUBLISH_RUN_ID}" --repo "${GITHUB_REPOSITORY}" --json workflowName,headBranch,event,status,conclusion,url)"
printf '%s' "${run_json}" | node scripts/validate-release-publish-approval.mjs
tag_sha="$(git rev-parse "${RELEASE_TAG}^{commit}")"
if [[ "${RELEASE_TARGET_SHA}" != "${tag_sha}" ]]; then
echo "Release target SHA ${RELEASE_TARGET_SHA} does not match ${RELEASE_TAG} (${tag_sha})." >&2
exit 1
fi
release_json="$(gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --json tagName,isDraft,isPrerelease,createdAt,assets,url)"
if [[ "$(printf '%s' "${release_json}" | jq -r '.tagName')" != "${RELEASE_TAG}" ]]; then
echo "GitHub release tag does not match ${RELEASE_TAG}." >&2
exit 1
fi
if [[ "$(printf '%s' "${release_json}" | jq -r '.isPrerelease')" == "true" ]]; then
echo "Android APK publishing requires a stable GitHub release." >&2
exit 1
fi
if [[ "${DIRECT_RELEASE_RECOVERY}" != "true" && "$(printf '%s' "${release_json}" | jq -r '.isDraft')" != "true" ]]; then
echo "Normal Android promotion requires the target GitHub release to remain a draft." >&2
exit 1
fi
release_created_at="$(printf '%s' "${release_json}" | jq -er '.createdAt')"
build_timestamp="$(date -u -d "${release_created_at}" +%Y-%m-%dT%H:%M:%SZ)"
echo "build_timestamp=${build_timestamp}" >> "${GITHUB_OUTPUT}"
unexpected_assets="$(printf '%s' "${release_json}" | jq -r '[.assets[]? | select(.name | startswith("OpenClaw-Android")) | .name] | join(", ")')"
if [[ -n "${unexpected_assets}" ]]; then
echo "Target release already contains Android assets: ${unexpected_assets}. Immutable recovery accepts them only after rebuilding identical bytes." >&2
fi
- name: Verify release source and Android version
id: release_source
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
release_version="${RELEASE_TAG#v}"
android_release_version="${release_version%%-*}"
package_version="$(node -p 'require("./package.json").version')"
android_version="$(node -p 'require("./apps/android/version.json").version')"
fallback_base_tag=""
fallback_base_sha=""
if [[ "${package_version}" == "${release_version}" ]]; then
:
elif [[ "${RELEASE_TAG}" =~ -[1-9][0-9]*$ && "${package_version}" == "${android_release_version}" ]]; then
fallback_base_tag="v${package_version}"
fallback_base_sha="$(git rev-parse "${fallback_base_tag}^{commit}")"
release_sha="$(git rev-parse HEAD)"
if [[ "${fallback_base_sha}" != "${release_sha}" ]]; then
echo "Fallback correction ${RELEASE_TAG} must resolve to the same source commit as ${fallback_base_tag}." >&2
exit 1
fi
else
echo "Release tag version ${release_version} does not match package.json ${package_version} or a same-commit fallback correction." >&2
exit 1
fi
if [[ "${android_version}" != "${android_release_version}" ]]; then
echo "Android version ${android_version} does not match release train ${android_release_version}." >&2
exit 1
fi
echo "fallback_base_tag=${fallback_base_tag}" >> "${GITHUB_OUTPUT}"
echo "FALLBACK_ANDROID_BASE_TAG=${fallback_base_tag}" >> "${GITHUB_ENV}"
echo "FALLBACK_ANDROID_BASE_SHA=${fallback_base_sha}" >> "${GITHUB_ENV}"
git diff --exit-code
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "true"
install-deps: "false"
use-actions-cache: "false"
- name: Setup Android toolchain
uses: ./.github/actions/setup-android-toolchain
- name: Create apps-signing read token
if: ${{ steps.release_source.outputs.fallback_base_tag == '' }}
id: apps-signing-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
owner: openclaw
repositories: apps-signing
permission-contents: read
- name: Checkout encrypted Android signing assets
if: ${{ steps.release_source.outputs.fallback_base_tag == '' }}
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: openclaw/apps-signing
ref: main
path: apps/android/build/release-signing/apps-signing
token: ${{ steps.apps-signing-token.outputs.token }}
persist-credentials: false
- name: Materialize Android release signing
if: ${{ steps.release_source.outputs.fallback_base_tag == '' }}
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
run: |
set -euo pipefail
if [[ -z "${MATCH_PASSWORD}" ]]; then
echo "Android release signing secrets are unavailable." >&2
exit 1
fi
node scripts/android-release-signing.mjs --mode materialize
- name: Prepare and verify signed Android APK
env:
GIT_COMMIT: ${{ inputs.release_target_sha }}
GH_TOKEN: ${{ github.token }}
OPENCLAW_BUILD_TIMESTAMP: ${{ steps.release_approval.outputs.build_timestamp }}
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
mkdir -p dist
if [[ -n "${FALLBACK_ANDROID_BASE_TAG}" ]]; then
base_dir="${RUNNER_TEMP}/fallback-android-release"
mkdir -p "${base_dir}"
gh release download "${FALLBACK_ANDROID_BASE_TAG}" --repo "${GITHUB_REPOSITORY}" \
--pattern OpenClaw-Android.apk \
--pattern OpenClaw-Android-SHA256SUMS.txt \
--dir "${base_dir}"
(
cd "${base_dir}"
sha256sum --strict --check OpenClaw-Android-SHA256SUMS.txt
)
gh attestation verify "${base_dir}/OpenClaw-Android.apk" \
--repo "${GITHUB_REPOSITORY}" \
--signer-workflow "${GITHUB_REPOSITORY}/.github/workflows/android-release.yml" \
--source-ref "refs/tags/${FALLBACK_ANDROID_BASE_TAG}" \
--source-digest "${FALLBACK_ANDROID_BASE_SHA}" \
--deny-self-hosted-runners
bun apps/android/scripts/build-release-artifacts.ts \
--verify-apk "${base_dir}/OpenClaw-Android.apk"
cp "${base_dir}/OpenClaw-Android.apk" dist/OpenClaw-Android.apk
cp "${base_dir}/OpenClaw-Android-SHA256SUMS.txt" dist/OpenClaw-Android-SHA256SUMS.txt
else
node --input-type=module <<'NODE'
import { readFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
const path = "apps/android/build/release-signing/gradle.properties";
const expected = new Set([
"OPENCLAW_ANDROID_STORE_FILE",
"OPENCLAW_ANDROID_STORE_PASSWORD",
"OPENCLAW_ANDROID_KEY_ALIAS",
"OPENCLAW_ANDROID_KEY_PASSWORD",
]);
const properties = new Map();
for (const rawLine of readFileSync(path, "utf8").split(/\r?\n/u)) {
const line = rawLine.trim();
if (!line || line.startsWith("#")) continue;
const separator = line.indexOf("=");
if (separator <= 0) throw new Error("Malformed Android signing property.");
properties.set(line.slice(0, separator).trim(), line.slice(separator + 1).trim());
}
for (const name of expected) {
const value = properties.get(name);
if (!value) throw new Error(`Missing Android signing property: ${name}`);
process.env[`ORG_GRADLE_PROJECT_${name}`] = value;
}
const result = spawnSync(
"bun",
["apps/android/scripts/build-release-artifacts.ts", "--artifact", "third-party"],
{ env: process.env, stdio: "inherit" },
);
process.exit(result.status ?? 1);
NODE
version="$(node -p 'require("./apps/android/version.json").version')"
source_apk="apps/android/build/release-artifacts/openclaw-${version}-third-party-release.apk"
source_checksum="${source_apk}.sha256"
test -s "${source_apk}"
test -s "${source_checksum}"
cp "${source_apk}" dist/OpenClaw-Android.apk
(
cd dist
sha256sum OpenClaw-Android.apk > OpenClaw-Android-SHA256SUMS.txt
)
fi
expected_version_name="${RELEASE_TAG#v}"
expected_version_name="${expected_version_name%%-*}"
expected_version_code="$(node -p 'require("./apps/android/version.json").versionCode')"
actual_app_id="$(apkanalyzer manifest application-id dist/OpenClaw-Android.apk)"
actual_version_name="$(apkanalyzer manifest version-name dist/OpenClaw-Android.apk)"
actual_version_code="$(apkanalyzer manifest version-code dist/OpenClaw-Android.apk)"
if [[ "${actual_app_id}" != "ai.openclaw.app" ]]; then
echo "Standalone APK has unexpected application ID ${actual_app_id}." >&2
exit 1
fi
if [[ "${actual_version_name}" != "${expected_version_name}" || "${actual_version_code}" != "${expected_version_code}" ]]; then
echo "Standalone APK version metadata does not match apps/android/version.json." >&2
exit 1
fi
if [[ -n "${FALLBACK_ANDROID_BASE_TAG}" ]]; then
echo "Reusing verified Android APK from ${FALLBACK_ANDROID_BASE_TAG} for same-commit fallback correction ${RELEASE_TAG}."
elif [[ "${RELEASE_TAG}" =~ -([1-9][0-9]*)$ ]]; then
correction_number="${BASH_REMATCH[1]}"
releases_json="$(gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/releases?per_page=100")"
previous_tag="$(printf '%s' "${releases_json}" | jq -r \
--arg base_tag "v${expected_version_name}" \
--argjson correction_number "${correction_number}" '
[
.[][]
| select(.draft == false and .prerelease == false)
| select(any(.assets[]?; .name == "OpenClaw-Android.apk"))
| select(any(.assets[]?; .name == "OpenClaw-Android-SHA256SUMS.txt"))
| if .tag_name == $base_tag then
{ tag: .tag_name, number: 0 }
elif (.tag_name | startswith($base_tag + "-")) then
(.tag_name | ltrimstr($base_tag + "-")) as $suffix
| select($suffix | test("^[1-9][0-9]*$"))
| { tag: .tag_name, number: ($suffix | tonumber) }
else empty end
| select(.number < $correction_number)
]
| sort_by(.number)
| last
| .tag // empty
')"
if [[ -n "${previous_tag}" ]]; then
previous_dir="${RUNNER_TEMP}/previous-android-release"
mkdir -p "${previous_dir}"
gh release download "${previous_tag}" --repo "${GITHUB_REPOSITORY}" \
--pattern OpenClaw-Android.apk \
--pattern OpenClaw-Android-SHA256SUMS.txt \
--dir "${previous_dir}"
(
cd "${previous_dir}"
sha256sum --strict --check OpenClaw-Android-SHA256SUMS.txt
)
gh attestation verify "${previous_dir}/OpenClaw-Android.apk" \
--repo "${GITHUB_REPOSITORY}" \
--signer-workflow "${GITHUB_REPOSITORY}/.github/workflows/android-release.yml" \
--source-ref "refs/tags/${previous_tag}" \
--deny-self-hosted-runners
bun apps/android/scripts/build-release-artifacts.ts \
--verify-apk "${previous_dir}/OpenClaw-Android.apk"
previous_version_code="$(apkanalyzer manifest version-code "${previous_dir}/OpenClaw-Android.apk")"
if (( actual_version_code <= previous_version_code )); then
echo "Android correction versionCode ${actual_version_code} must exceed ${previous_tag} versionCode ${previous_version_code}." >&2
exit 1
fi
else
echo "No earlier verified standalone APK exists for ${expected_version_name}; accepting this correction as the standalone channel bootstrap."
fi
fi
- name: Attest Android APK provenance
uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0
with:
subject-path: dist/OpenClaw-Android.apk
- name: Upload immutable Android release assets
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
attach_or_verify_asset() {
local source_path="$1"
local asset_name="$2"
local existing_dir="${RUNNER_TEMP}/existing-${asset_name}"
if gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --json assets |
jq -e --arg name "${asset_name}" 'any(.assets[]?; .name == $name)' >/dev/null; then
rm -rf "${existing_dir}"
mkdir -p "${existing_dir}"
gh release download "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" \
--pattern "${asset_name}" --dir "${existing_dir}"
cmp --silent "${source_path}" "${existing_dir}/${asset_name}" || {
echo "Existing Android release asset ${asset_name} differs from this exact-tag build." >&2
exit 1
}
return
fi
gh release upload "${RELEASE_TAG}" "${source_path}#${asset_name}" --repo "${GITHUB_REPOSITORY}"
}
attach_or_verify_asset dist/OpenClaw-Android.apk OpenClaw-Android.apk
attach_or_verify_asset dist/OpenClaw-Android-SHA256SUMS.txt OpenClaw-Android-SHA256SUMS.txt
- name: Verify published Android release assets
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
verify_dir="${RUNNER_TEMP}/verify-android-release"
mkdir -p "${verify_dir}"
gh release download "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" \
--pattern OpenClaw-Android.apk \
--pattern OpenClaw-Android-SHA256SUMS.txt \
--dir "${verify_dir}"
(
cd "${verify_dir}"
sha256sum --strict --check OpenClaw-Android-SHA256SUMS.txt
)
gh attestation verify "${verify_dir}/OpenClaw-Android.apk" \
--repo "${GITHUB_REPOSITORY}" \
--signer-workflow "${GITHUB_REPOSITORY}/.github/workflows/android-release.yml" \
--source-ref "refs/tags/${RELEASE_TAG}" \
--deny-self-hosted-runners
bun apps/android/scripts/build-release-artifacts.ts \
--verify-apk "${verify_dir}/OpenClaw-Android.apk"
release_json="$(gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --json assets)"
expected_names='["OpenClaw-Android-SHA256SUMS.txt","OpenClaw-Android.apk"]'
actual_names="$(printf '%s' "${release_json}" | jq -c '[.assets[]? | select(.name | startswith("OpenClaw-Android")) | .name] | sort')"
if [[ "${actual_names}" != "${expected_names}" ]]; then
echo "Android release asset names do not match the canonical contract." >&2
exit 1
fi
- name: Remove materialized signing files
if: ${{ always() }}
run: rm -rf apps/android/build/release-signing
- name: Summary
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
{
echo "## Signed Android APK published"
echo
echo "- APK: https://github.com/${GITHUB_REPOSITORY}/releases/download/${RELEASE_TAG}/OpenClaw-Android.apk"
echo "- Checksums: https://github.com/${GITHUB_REPOSITORY}/releases/download/${RELEASE_TAG}/OpenClaw-Android-SHA256SUMS.txt"
echo "- Provenance: \`gh attestation verify OpenClaw-Android.apk --repo ${GITHUB_REPOSITORY} --signer-workflow ${GITHUB_REPOSITORY}/.github/workflows/android-release.yml\`"
} >> "${GITHUB_STEP_SUMMARY}"