Skip to content

Commit 98c570f

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents f3db42a + 6a0ae78 commit 98c570f

8 files changed

Lines changed: 278 additions & 2 deletions

File tree

.github/workflows/build-desktop-tauri.yml

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,75 @@ jobs:
266266
with:
267267
astrbot_version: ${{ steps.desktop_version.outputs.normalized }}
268268

269+
- name: Import Apple Developer Certificate
270+
id: import_apple_certificate
271+
env:
272+
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
273+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
274+
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
275+
shell: bash
276+
run: |
277+
set -euo pipefail
278+
if [ -z "${APPLE_CERTIFICATE:-}" ]; then
279+
echo "APPLE_CERTIFICATE is not configured; skipping certificate import."
280+
printf 'signing_identity=\n' >> "$GITHUB_OUTPUT"
281+
exit 0
282+
fi
283+
CERT_PATH="$RUNNER_TEMP/certificate.p12"
284+
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain"
285+
286+
echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH"
287+
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
288+
security default-keychain -s "$KEYCHAIN_PATH"
289+
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
290+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
291+
security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
292+
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
293+
rm -f "$CERT_PATH"
294+
295+
signing_identities=()
296+
while IFS= read -r identity; do
297+
signing_identities+=("$identity")
298+
done < <(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep "Developer ID Application" || true)
299+
if [ "${#signing_identities[@]}" -eq 0 ]; then
300+
echo "::error::No Developer ID Application identity found in the imported keychain." >&2
301+
security find-identity -v -p codesigning "$KEYCHAIN_PATH" || true
302+
exit 1
303+
fi
304+
if [ "${#signing_identities[@]}" -ne 1 ]; then
305+
echo "::error::Expected exactly one Developer ID Application identity, found ${#signing_identities[@]}." >&2
306+
printf '%s\n' "${signing_identities[@]}" >&2
307+
exit 1
308+
fi
309+
CERT_ID=$(printf '%s\n' "${signing_identities[0]}" | awk -F'"' '{print $2}')
310+
if [ -z "$CERT_ID" ]; then
311+
echo "::error::Failed to parse the imported Developer ID Application identity." >&2
312+
exit 1
313+
fi
314+
printf 'APPLE_SIGNING_IDENTITY=%s\n' "$CERT_ID" >> "$GITHUB_ENV"
315+
printf 'signing_identity=%s\n' "$CERT_ID" >> "$GITHUB_OUTPUT"
316+
echo "Imported signing identity: $CERT_ID"
317+
318+
- name: Pre-sign backend resources (macOS)
319+
if: ${{ steps.import_apple_certificate.outputs.signing_identity != '' }}
320+
env:
321+
ASTRBOT_SOURCE_GIT_URL: ${{ needs.resolve_build_context.outputs.source_git_url }}
322+
ASTRBOT_SOURCE_GIT_REF: ${{ needs.resolve_build_context.outputs.source_git_ref }}
323+
ASTRBOT_DESKTOP_VERSION: ${{ steps.desktop_version.outputs.prefixed }}
324+
ASTRBOT_DESKTOP_UPDATER_PUBLIC_KEY: ${{ env.ASTRBOT_DESKTOP_UPDATER_PUBLIC_KEY }}
325+
GITHUB_TOKEN: ${{ github.token }}
326+
GH_TOKEN: ${{ github.token }}
327+
shell: bash
328+
run: |
329+
set -euo pipefail
330+
pnpm run prepare:resources
331+
if [ ! -d "resources/backend" ]; then
332+
echo "::error::resources/backend not found after prepare:resources." >&2
333+
exit 1
334+
fi
335+
echo "Pre-signing Mach-O binaries in resources/backend..."
336+
bash scripts/ci/codesign-macos-nested.sh "resources/backend" "src-tauri/entitlements.plist"
337+
269338
- name: Resolve macOS app bundle name
270339
id: resolve_macos_app_bundle
271340
env:
@@ -295,6 +364,11 @@ jobs:
295364
ASTRBOT_DESKTOP_CRYPTOGRAPHY_FALLBACK_VERSIONS: ${{ vars.ASTRBOT_DESKTOP_CRYPTOGRAPHY_FALLBACK_VERSIONS || '' }}
296365
ASTRBOT_MACOS_BUILD_MAX_ATTEMPTS: ${{ vars.ASTRBOT_MACOS_BUILD_MAX_ATTEMPTS || '3' }}
297366
ASTRBOT_MACOS_BUILD_RETRY_SLEEP_SECONDS: ${{ vars.ASTRBOT_MACOS_BUILD_RETRY_SLEEP_SECONDS || '8' }}
367+
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
368+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
369+
APPLE_ID: ${{ secrets.APPLE_ID }}
370+
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
371+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
298372
GITHUB_TOKEN: ${{ github.token }}
299373
GH_TOKEN: ${{ github.token }}
300374
shell: bash
@@ -306,6 +380,8 @@ jobs:
306380
307381
max_attempts="${ASTRBOT_MACOS_BUILD_MAX_ATTEMPTS}"
308382
retry_sleep_seconds="${ASTRBOT_MACOS_BUILD_RETRY_SLEEP_SECONDS}"
383+
# Resources are already prepared and pre-signed in the previous step.
384+
tauri_config_override='{"build":{"beforeBuildCommand":""}}'
309385
# Retry only for known transient cargo/crates network failures.
310386
# Spurious retry hints emitted by cargo.
311387
transient_retry_spurious='spurious network error|network failure seems to have happened'
@@ -330,7 +406,7 @@ jobs:
330406
331407
for attempt in $(seq 1 "${max_attempts}"); do
332408
build_log="$(mktemp -t tauri-macos-build.XXXXXX.log)"
333-
if cargo tauri build --verbose --target ${{ matrix.target }} --bundles app 2>&1 | tee "${build_log}"; then
409+
if cargo tauri build --verbose --target ${{ matrix.target }} --bundles app --config "${tauri_config_override}" 2>&1 | tee "${build_log}"; then
334410
rm -f "${build_log}" || true
335411
break
336412
fi
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
if [ -z "${APPLE_SIGNING_IDENTITY:-}" ]; then
5+
echo "::warning::APPLE_SIGNING_IDENTITY is not set; skipping code signing."
6+
exit 0
7+
fi
8+
9+
TARGET="${1:?Usage: $0 <path-to-directory-or-app-bundle> [entitlements.plist]}"
10+
ENTITLEMENTS_PATH="${2:-}"
11+
ENTITLEMENTS_ARGS=()
12+
if [ -n "${ENTITLEMENTS_PATH}" ] && [ -f "${ENTITLEMENTS_PATH}" ]; then
13+
ENTITLEMENTS_ARGS=(--entitlements "${ENTITLEMENTS_PATH}")
14+
fi
15+
16+
SIGN_OPTS=(--force --options runtime --sign "${APPLE_SIGNING_IDENTITY}")
17+
18+
if [ ! -d "${TARGET}" ]; then
19+
echo "::error::Target not found: ${TARGET}" >&2
20+
exit 1
21+
fi
22+
23+
BINARY_FILE_DESCRIPTIONS=()
24+
NESTED_BINARIES=()
25+
while IFS= read -r -d '' file; do
26+
file_description="$(file --brief "${file}")"
27+
if [[ "${file_description}" == *"Mach-O"* ]]; then
28+
NESTED_BINARIES+=("${file}")
29+
BINARY_FILE_DESCRIPTIONS+=("${file_description}")
30+
fi
31+
done < <(find "${TARGET}" -type f -print0 2>/dev/null)
32+
33+
if [ "${#NESTED_BINARIES[@]}" -eq 0 ]; then
34+
echo "No Mach-O binaries found in ${TARGET}; nothing to sign."
35+
exit 0
36+
fi
37+
38+
echo "Found ${#NESTED_BINARIES[@]} Mach-O binary(ies) to sign in ${TARGET}."
39+
40+
for index in "${!NESTED_BINARIES[@]}"; do
41+
binary="${NESTED_BINARIES[${index}]}"
42+
file_description="${BINARY_FILE_DESCRIPTIONS[${index}]}"
43+
echo " Signing: ${binary}"
44+
# Only apply entitlements to executables, not dylibs
45+
CURRENT_ENTITLEMENTS=()
46+
if [ ${#ENTITLEMENTS_ARGS[@]} -gt 0 ] && [[ "${file_description}" == *"executable"* ]]; then
47+
CURRENT_ENTITLEMENTS=("${ENTITLEMENTS_ARGS[@]}")
48+
fi
49+
codesign "${SIGN_OPTS[@]}" \
50+
${CURRENT_ENTITLEMENTS[@]+"${CURRENT_ENTITLEMENTS[@]}"} \
51+
"${binary}"
52+
done
53+
54+
echo "Code signing completed successfully for ${#NESTED_BINARIES[@]} binary(ies)."

scripts/ci/lib/nightly_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77

88
SPEC_PATH = Path(__file__).resolve().parents[3] / "src-tauri" / "nightly-version-format.json"
9-
BASE_VERSION_PATTERN = r"[0-9]+(?:\.[0-9]+){1,2}"
9+
BASE_VERSION_PATTERN = r"[0-9]+(?:\.[0-9]+){1,2}(?:-[a-zA-Z0-9.]+)?"
1010

1111
_SPEC = json.loads(SPEC_PATH.read_text(encoding="utf-8"))
1212
NIGHTLY_CANONICAL_FORMAT = str(_SPEC["canonicalFormat"])

scripts/prepare-resources/desktop-bridge-checks.mjs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,49 @@ export const patchMonacoCssNestingWarnings = async ({ dashboardDir, projectRoot
6060
}
6161
};
6262

63+
export const patchDesktopReleaseUpdateIndicator = async ({ dashboardDir, projectRoot }) => {
64+
const file = path.join(
65+
dashboardDir,
66+
'src',
67+
'layouts',
68+
'full',
69+
'vertical-header',
70+
'VerticalHeader.vue',
71+
);
72+
if (!existsSync(file)) {
73+
return;
74+
}
75+
76+
const source = await readFile(file, 'utf8');
77+
if (source.includes('const backendHasNewVersion = !isDesktopReleaseMode.value && res.data.data.has_new_version;')) {
78+
return;
79+
}
80+
81+
const targetPattern =
82+
/^(\s*)hasNewVersion\.value\s*=\s*res\.data\.data\.has_new_version;\s*\n\s*if\s*\(\s*res\.data\.data\.has_new_version\s*\)\s*\{/m;
83+
84+
if (!targetPattern.test(source)) {
85+
console.warn(
86+
`[prepare-resources] Could not patch desktop release update banner gating in ${path.relative(projectRoot, file)} because the expected update check pattern was not found.`,
87+
);
88+
return;
89+
}
90+
91+
const patched = source.replace(
92+
targetPattern,
93+
(_, indent) =>
94+
`${indent}const backendHasNewVersion = !isDesktopReleaseMode.value && res.data.data.has_new_version;\n` +
95+
`${indent}hasNewVersion.value = backendHasNewVersion;\n\n` +
96+
`${indent}if (backendHasNewVersion) {`,
97+
);
98+
if (patched !== source) {
99+
await writeFile(file, patched, 'utf8');
100+
console.log(
101+
`[prepare-resources] Patched desktop release update banner gating in ${path.relative(projectRoot, file)}`,
102+
);
103+
}
104+
};
105+
63106
export const verifyDesktopBridgeArtifacts = async ({
64107
dashboardDir,
65108
projectRoot,

scripts/prepare-resources/desktop-bridge-checks.test.mjs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { test } from 'node:test';
22
import assert from 'node:assert/strict';
3+
import { mkdtemp, mkdir, readFile, writeFile } from 'node:fs/promises';
4+
import { tmpdir } from 'node:os';
5+
import path from 'node:path';
36

47
import {
58
getDesktopBridgeExpectations,
69
shouldEnforceDesktopBridgeExpectation,
710
} from './desktop-bridge-expectations.mjs';
11+
import { patchDesktopReleaseUpdateIndicator } from './desktop-bridge-checks.mjs';
812

913
test('getDesktopBridgeExpectations returns stable expectation metadata', () => {
1014
const expectations = getDesktopBridgeExpectations();
@@ -60,3 +64,85 @@ test('shouldEnforceDesktopBridgeExpectation enforces required expectations on no
6064
true,
6165
);
6266
});
67+
68+
test('patchDesktopReleaseUpdateIndicator tolerates minor formatting changes in the upstream source', async () => {
69+
const dashboardDir = await mkdtemp(path.join(tmpdir(), 'astrbot-dashboard-'));
70+
const headerFile = path.join(
71+
dashboardDir,
72+
'src',
73+
'layouts',
74+
'full',
75+
'vertical-header',
76+
'VerticalHeader.vue',
77+
);
78+
await mkdir(path.dirname(headerFile), { recursive: true });
79+
await writeFile(
80+
headerFile,
81+
`function checkUpdate() {
82+
axios.get('/api/update/check')
83+
.then((res) => {
84+
hasNewVersion.value = res.data.data.has_new_version;
85+
if ( res.data.data.has_new_version ) {
86+
releaseMessage.value = res.data.message;
87+
updateStatus.value = t('core.header.version.hasNewVersion');
88+
} else {
89+
updateStatus.value = res.data.message;
90+
}
91+
dashboardHasNewVersion.value = isDesktopReleaseMode.value
92+
? false
93+
: res.data.data.dashboard_has_new_version;
94+
})
95+
}
96+
`,
97+
'utf8',
98+
);
99+
100+
await patchDesktopReleaseUpdateIndicator({ dashboardDir, projectRoot: dashboardDir });
101+
102+
const patched = await readFile(headerFile, 'utf8');
103+
assert.match(
104+
patched,
105+
/const backendHasNewVersion = !isDesktopReleaseMode\.value && res\.data\.data\.has_new_version;/,
106+
);
107+
assert.match(patched, /hasNewVersion\.value = backendHasNewVersion;/);
108+
assert.match(patched, /if \(backendHasNewVersion\) \{/);
109+
assert.doesNotMatch(patched, /hasNewVersion\.value = res\.data\.data\.has_new_version;/);
110+
});
111+
112+
test('patchDesktopReleaseUpdateIndicator warns when the expected update pattern is missing', async () => {
113+
const dashboardDir = await mkdtemp(path.join(tmpdir(), 'astrbot-dashboard-'));
114+
const headerFile = path.join(
115+
dashboardDir,
116+
'src',
117+
'layouts',
118+
'full',
119+
'vertical-header',
120+
'VerticalHeader.vue',
121+
);
122+
await mkdir(path.dirname(headerFile), { recursive: true });
123+
await writeFile(
124+
headerFile,
125+
`function checkUpdate() {
126+
axios.get('/api/update/check')
127+
.then((res) => {
128+
updateStatus.value = res.data.message;
129+
})
130+
}
131+
`,
132+
'utf8',
133+
);
134+
135+
const warnings = [];
136+
const originalWarn = console.warn;
137+
console.warn = (message) => warnings.push(String(message));
138+
139+
try {
140+
await patchDesktopReleaseUpdateIndicator({ dashboardDir, projectRoot: dashboardDir });
141+
} finally {
142+
console.warn = originalWarn;
143+
}
144+
145+
assert.equal(warnings.length, 1);
146+
assert.match(warnings[0], /Could not patch desktop release update banner gating/);
147+
assert.match(warnings[0], /VerticalHeader\.vue/);
148+
});

scripts/prepare-resources/mode-tasks.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs';
33
import path from 'node:path';
44
import { spawnSync } from 'node:child_process';
55
import {
6+
patchDesktopReleaseUpdateIndicator,
67
patchMonacoCssNestingWarnings,
78
verifyDesktopBridgeArtifacts,
89
} from './desktop-bridge-checks.mjs';
@@ -66,6 +67,7 @@ export const prepareWebui = async ({
6667
const dashboardDir = path.join(sourceDir, 'dashboard');
6768
ensurePackageInstall(dashboardDir, 'AstrBot dashboard');
6869
await patchMonacoCssNestingWarnings({ dashboardDir, projectRoot });
70+
await patchDesktopReleaseUpdateIndicator({ dashboardDir, projectRoot });
6971
await verifyDesktopBridgeArtifacts({
7072
dashboardDir,
7173
projectRoot,

src-tauri/entitlements.plist

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.cs.allow-jit</key>
6+
<true/>
7+
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
8+
<true/>
9+
<key>com.apple.security.cs.disable-library-validation</key>
10+
<true/>
11+
</dict>
12+
</plist>

src-tauri/tauri.conf.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
"active": true,
3232
"createUpdaterArtifacts": true,
3333
"targets": "all",
34+
"macOS": {
35+
"entitlements": "entitlements.plist"
36+
},
3437
"windows": {
3538
"webviewInstallMode": {
3639
"type": "downloadBootstrapper",

0 commit comments

Comments
 (0)