Skip to content

Commit d0c32a2

Browse files
committed
ci(release): 允许无签名产物继续发布
1 parent 45f3992 commit d0c32a2

14 files changed

Lines changed: 106 additions & 83 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ jobs:
9292
format: sarif
9393
output: trivy-results.sarif
9494
severity: 'CRITICAL,HIGH'
95+
ignore-unfixed: true
9596
exit-code: '0'
9697

9798
- name: Upload Trivy SARIF

.github/workflows/release.yml

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -176,33 +176,43 @@ jobs:
176176
with:
177177
gradle-home-cache-cleanup: true
178178

179-
- name: Decode Android signing key
180-
shell: bash
181-
run: |
182-
if [ -n "$ANDROID_KEYSTORE_BASE64" ]; then
183-
echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > release.keystore
184-
fi
185-
env:
186-
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
187-
188-
- name: Validate Android signing secrets
179+
- name: Prepare Android signing key
189180
shell: bash
190181
run: |
182+
signing_values=(
183+
"$ANDROID_KEYSTORE_BASE64"
184+
"$ANDROID_KEYSTORE_PASSWORD"
185+
"$ANDROID_KEY_ALIAS"
186+
"$ANDROID_KEY_PASSWORD"
187+
)
188+
configured=0
189+
for value in "${signing_values[@]}"; do
190+
if [ -n "$value" ]; then
191+
configured=1
192+
fi
193+
done
191194
missing=()
192195
for name in ANDROID_KEYSTORE_BASE64 ANDROID_KEYSTORE_PASSWORD ANDROID_KEY_ALIAS ANDROID_KEY_PASSWORD; do
193196
if [ -z "${!name}" ]; then
194197
missing+=("$name")
195198
fi
196199
done
200+
if [ "$configured" -eq 0 ]; then
201+
echo "::notice::Android signing secrets are not configured; building an unsigned release APK."
202+
echo "TASKBRIDGE_ANDROID_SIGNED_RELEASE=false" >> "$GITHUB_ENV"
203+
exit 0
204+
fi
197205
if [ "${#missing[@]}" -gt 0 ]; then
198-
echo "Missing Android signing secrets: ${missing[*]}" >&2
199-
echo "Public release artifacts must be signed; configure all Android signing secrets." >&2
206+
echo "::error::Incomplete Android signing secrets: ${missing[*]}."
207+
echo "Configure all Android signing secrets or leave all of them empty for an unsigned APK." >&2
200208
exit 1
201209
fi
210+
echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > release.keystore
202211
if [ ! -s release.keystore ]; then
203212
echo "Decoded Android signing keystore is empty or missing." >&2
204213
exit 1
205214
fi
215+
echo "TASKBRIDGE_ANDROID_SIGNED_RELEASE=true" >> "$GITHUB_ENV"
206216
env:
207217
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
208218
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
@@ -226,6 +236,10 @@ jobs:
226236
- name: Verify Android APK signature
227237
shell: bash
228238
run: |
239+
if [ "$TASKBRIDGE_ANDROID_SIGNED_RELEASE" != "true" ]; then
240+
echo "::notice::Skipping Android APK signature verification because this release is unsigned."
241+
exit 0
242+
fi
229243
apk_path="$(find app/build/outputs/apk/release -maxdepth 1 -type f -name 'app-release.apk' | head -n 1)"
230244
if [ -z "$apk_path" ]; then
231245
echo "No release APK was produced." >&2
@@ -248,12 +262,18 @@ jobs:
248262
run: |
249263
mkdir -p ../artifacts
250264
find app/build/outputs/apk -maxdepth 3 -type f -print
251-
apk_path="$(find app/build/outputs/apk/release -maxdepth 1 -type f -name 'app-release.apk' | head -n 1)"
265+
if [ "$TASKBRIDGE_ANDROID_SIGNED_RELEASE" = "true" ]; then
266+
apk_path="$(find app/build/outputs/apk/release -maxdepth 1 -type f -name 'app-release.apk' | head -n 1)"
267+
artifact_name="TaskBridge-${RELEASE_VERSION}-android.apk"
268+
else
269+
apk_path="$(find app/build/outputs/apk/release -maxdepth 1 -type f -name 'app-release-unsigned.apk' | head -n 1)"
270+
artifact_name="TaskBridge-${RELEASE_VERSION}-android-unsigned.apk"
271+
fi
252272
if [ -z "$apk_path" ]; then
253273
echo "No release APK was produced." >&2
254274
exit 1
255275
fi
256-
cp "$apk_path" "../artifacts/TaskBridge-${RELEASE_VERSION}-android.apk"
276+
cp "$apk_path" "../artifacts/${artifact_name}"
257277
258278
- name: Upload Android artifact
259279
uses: actions/upload-artifact@v4
@@ -414,21 +434,28 @@ jobs:
414434
- name: Smoke test Web client shell
415435
run: node ..\scripts\smoke-web-client.mjs
416436

417-
- name: Validate Windows signing secrets
437+
- name: Prepare Windows signing certificate
418438
shell: pwsh
419439
run: |
420-
if ([string]::IsNullOrWhiteSpace($env:WINDOWS_CERTIFICATE_BASE64)) {
421-
throw "WINDOWS_CERTIFICATE_BASE64 must be configured for public release artifacts."
440+
$hasCertificate = -not [string]::IsNullOrWhiteSpace($env:WINDOWS_CERTIFICATE_BASE64)
441+
$hasPassword = -not [string]::IsNullOrWhiteSpace($env:WINDOWS_CERTIFICATE_PASSWORD)
442+
if (-not $hasCertificate -and -not $hasPassword) {
443+
"TASKBRIDGE_WINDOWS_SIGNED_RELEASE=false" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
444+
"CSC_IDENTITY_AUTO_DISCOVERY=false" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
445+
Write-Host "::notice::Windows signing certificate is not configured; building an unsigned installer."
446+
exit 0
422447
}
423-
if ([string]::IsNullOrWhiteSpace($env:WINDOWS_CERTIFICATE_PASSWORD)) {
424-
throw "WINDOWS_CERTIFICATE_PASSWORD must be configured for public release artifacts."
448+
if (-not $hasCertificate -or -not $hasPassword) {
449+
throw "Incomplete Windows signing secrets. Configure both WINDOWS_CERTIFICATE_BASE64 and WINDOWS_CERTIFICATE_PASSWORD, or leave both empty for an unsigned installer."
425450
}
426451
$certificatePath = Join-Path $env:RUNNER_TEMP "taskbridge-codesign.pfx"
427452
[IO.File]::WriteAllBytes($certificatePath, [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE_BASE64))
428453
if ((Get-Item -LiteralPath $certificatePath).Length -le 0) {
429454
throw "Decoded Windows signing certificate is empty."
430455
}
431456
"CSC_LINK=$certificatePath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
457+
"CSC_KEY_PASSWORD=$env:WINDOWS_CERTIFICATE_PASSWORD" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
458+
"TASKBRIDGE_WINDOWS_SIGNED_RELEASE=true" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
432459
env:
433460
WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}
434461
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
@@ -438,14 +465,16 @@ jobs:
438465
env:
439466
TASKBRIDGE_BASE_URL: ${{ env.TASKBRIDGE_BASE_URL }}
440467
TASKBRIDGE_WS_URL: ${{ env.TASKBRIDGE_WS_URL }}
441-
CSC_LINK: ${{ runner.temp }}\\taskbridge-codesign.pfx
442-
CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
443468
ELECTRON_CACHE: ${{ github.workspace }}\\.cache\\electron
444469
ELECTRON_BUILDER_CACHE: ${{ github.workspace }}\\.cache\\electron-builder
445470

446471
- name: Verify Windows installer signatures
447472
shell: pwsh
448473
run: |
474+
if ($env:TASKBRIDGE_WINDOWS_SIGNED_RELEASE -ne "true") {
475+
Write-Host "::notice::Skipping Windows installer signature verification because this release is unsigned."
476+
exit 0
477+
}
449478
$installers = @(Get-ChildItem -Path release -File | Where-Object { $_.Extension -eq '.exe' })
450479
if ($installers.Count -eq 0) {
451480
throw "No Windows installer .exe was produced."
@@ -569,6 +598,7 @@ jobs:
569598
with:
570599
image-ref: taskbridge:release-scan
571600
severity: 'CRITICAL,HIGH'
601+
ignore-unfixed: true
572602
exit-code: '1'
573603

574604
- name: Build and push backend image

android/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ Release APK:
8484
.\gradlew.bat :app:assembleRelease
8585
```
8686

87-
Release 构建必须配置正式签名。缺少 keystore、密码、alias key 密码时`assembleRelease` 会直接失败,避免误发布 debug 签名 APK。
87+
Release 构建会优先使用正式签名。配置完整 keystore、密码、alias key 密码时会生成已签名 APK;缺少签名配置时`assembleRelease` 会生成 unsigned release APK,不会回退到 debug signing
8888

89-
本机临时验证请使用 debug 构建;release 产物必须具备正式签名,不再提供未签名绕过入口
89+
本机临时验证仍建议使用 debug 构建;如果需要发布 unsigned release,请在文件名和说明中明确标注,避免用户把它当作已签名正式 APK
9090

9191
## Release 签名
9292

android/app/build.gradle.kts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,6 @@ android {
120120
it.name == "bundleRelease" ||
121121
it.name == "packageRelease"
122122
}
123-
if (requiresSignedRelease && !hasReleaseSigning) {
124-
throw org.gradle.api.GradleException(
125-
"Release signing is required. Configure ANDROID_KEYSTORE_PATH, " +
126-
"ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS and ANDROID_KEY_PASSWORD.",
127-
)
128-
}
129123
if (requiresSignedRelease && releaseEndpointLooksPlaceholder) {
130124
throw org.gradle.api.GradleException(
131125
"Release endpoints must be configured with TASKBRIDGE_BASE_URL and TASKBRIDGE_WS_URL.",

backend/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
alembic>=1.13,<2.0
22
fastapi>=0.115,<1.0
3+
jaraco.context>=6.1.0,<7.0
34
passlib[bcrypt]>=1.7,<2.0
45
bcrypt>=3.2,<4.0
56
pydantic>=2.9,<3.0

desktop/scripts/check-production-hardening-config.mjs

Lines changed: 18 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,18 @@ for (const [workflowName, source] of [
5050
}
5151

5252
assert.match(backendDockerfileSource, /\/ready/, "backend container healthcheck must use the readiness endpoint");
53-
assert.match(releaseSource, /Validate Windows signing secrets/, "release workflow must validate Windows signing secrets");
54-
assert.match(releaseSource, /WINDOWS_CERTIFICATE_BASE64/, "release workflow must require a Windows signing certificate");
55-
assert.match(releaseSource, /CSC_LINK/, "release workflow must pass a signing certificate to electron-builder");
56-
assert.match(releaseSource, /CSC_KEY_PASSWORD/, "release workflow must pass the Windows signing password to electron-builder");
53+
assert.match(releaseSource, /Prepare Windows signing certificate/, "release workflow must prepare optional Windows signing");
54+
assert.match(releaseSource, /TASKBRIDGE_WINDOWS_SIGNED_RELEASE/, "release workflow must track whether Windows artifacts are signed");
55+
assert.match(releaseSource, /CSC_IDENTITY_AUTO_DISCOVERY=false/, "release workflow must explicitly disable Windows signing when no certificate is configured");
56+
assert.match(releaseSource, /CSC_LINK/, "release workflow must pass a signing certificate to electron-builder when configured");
57+
assert.match(releaseSource, /CSC_KEY_PASSWORD/, "release workflow must pass the Windows signing password to electron-builder when configured");
5758
assert.match(
5859
releaseSource,
59-
/Verify Windows installer signatures[\s\S]*Get-AuthenticodeSignature[\s\S]*Status -ne 'Valid'/,
60-
"release workflow must verify Windows Authenticode signatures before uploading artifacts",
60+
/Verify Windows installer signatures[\s\S]*TASKBRIDGE_WINDOWS_SIGNED_RELEASE[\s\S]*Get-AuthenticodeSignature[\s\S]*Status -ne 'Valid'/,
61+
"release workflow must verify Windows Authenticode signatures when Windows signing is configured",
6162
);
62-
assert.match(releaseSource, /Validate Android signing secrets/, "release workflow must validate Android signing secrets");
63+
assert.match(releaseSource, /Prepare Android signing key/, "release workflow must prepare optional Android signing");
64+
assert.match(releaseSource, /TASKBRIDGE_ANDROID_SIGNED_RELEASE/, "release workflow must track whether Android artifacts are signed");
6365
for (const token of [
6466
"ANDROID_KEYSTORE_BASE64",
6567
"ANDROID_KEYSTORE_PASSWORD",
@@ -71,39 +73,17 @@ for (const token of [
7173
}
7274
assert.match(
7375
releaseSource,
74-
/Verify Android APK signature[\s\S]*apksigner[\s\S]*verify --verbose --print-certs/,
75-
"release workflow must verify the Android APK signature before uploading artifacts",
76+
/Verify Android APK signature[\s\S]*TASKBRIDGE_ANDROID_SIGNED_RELEASE[\s\S]*apksigner[\s\S]*verify --verbose --print-certs/,
77+
"release workflow must verify the Android APK signature when Android signing is configured",
7678
);
77-
assert.doesNotMatch(
78-
releaseSource,
79-
/TASKBRIDGE_ALLOW_UNSIGNED_RELEASE/,
80-
"public release workflow must not allow unsigned Android release artifacts",
81-
);
82-
assert.doesNotMatch(
83-
androidBuildSource,
84-
/TASKBRIDGE_ALLOW_UNSIGNED_RELEASE|allowUnsignedRelease/,
85-
"Android release builds must not expose an unsigned release escape hatch",
86-
);
87-
assert.doesNotMatch(
88-
androidReadmeSource,
89-
/TASKBRIDGE_ALLOW_UNSIGNED_RELEASE|unsigned release/i,
90-
"Android docs must not teach unsigned release builds",
91-
);
92-
assert.doesNotMatch(
93-
troubleshootingSource,
94-
/unsigned release APK/i,
95-
"troubleshooting docs must direct local experiments to debug builds, not unsigned release APKs",
96-
);
97-
assert.doesNotMatch(
98-
releaseSource,
99-
/app-release-unsigned\.apk/,
100-
"public release workflow must not publish unsigned Android release artifacts",
101-
);
102-
assert.doesNotMatch(
79+
assert.match(
10380
releaseSource,
104-
/CSC_IDENTITY_AUTO_DISCOVERY:\s*["']false["']/,
105-
"public release workflow must not disable Windows signing",
81+
/app-release-unsigned\.apk[\s\S]*android-unsigned\.apk/,
82+
"release workflow must publish a clearly named unsigned Android APK when signing is not configured",
10683
);
84+
assert.doesNotMatch(androidBuildSource, /Release signing is required/, "Android release builds must not fail solely because signing is not configured");
85+
assert.match(androidReadmeSource, /unsigned release/i, "Android docs must explain unsigned release artifacts");
86+
assert.match(troubleshootingSource, /unsigned release APK/i, "troubleshooting docs must explain unsigned release APKs");
10787

10888
for (const token of [
10989
"docker compose",
@@ -144,6 +124,7 @@ for (const token of [
144124
"WINDOWS_CERTIFICATE_BASE64",
145125
"WINDOWS_CERTIFICATE_PASSWORD",
146126
"unsigned Android",
127+
"unsigned Windows",
147128
]) {
148129
assert.match(releaseDocsSource, new RegExp(escapeRegExp(token)), `release docs must document ${token}`);
149130
}
@@ -171,4 +152,3 @@ for (const token of [
171152
}
172153

173154
console.log("production hardening config check passed");
174-

desktop/scripts/check-security-config.mjs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,30 @@ assert.doesNotMatch(
4747
/signingConfigs\.getByName\("debug"\)/,
4848
"Android release builds must not fall back to the debug signing key",
4949
);
50-
assert.match(
50+
assert.doesNotMatch(
5151
androidBuildSource,
5252
/Release signing is required/,
53-
"Android release assemble/bundle tasks must fail clearly when signing secrets are missing",
53+
"Android release assemble/bundle tasks must allow explicitly unsigned release APKs",
54+
);
55+
assert.match(
56+
androidBuildSource,
57+
/if \(hasReleaseSigning\)[\s\S]*signingConfig = signingConfigs\.getByName\("release"\)/,
58+
"Android release builds must use the release signing config when signing secrets are configured",
59+
);
60+
assert.match(
61+
releaseWorkflowSource,
62+
/Prepare Android signing key/,
63+
"GitHub release workflow must prepare Android signing before publishing an APK",
64+
);
65+
assert.match(
66+
releaseWorkflowSource,
67+
/TASKBRIDGE_ANDROID_SIGNED_RELEASE/,
68+
"GitHub release workflow must track signed and unsigned Android artifacts",
5469
);
5570
assert.match(
5671
releaseWorkflowSource,
57-
/Validate Android signing secrets/,
58-
"GitHub release workflow must validate Android signing secrets before publishing an APK",
72+
/Incomplete Android signing secrets/,
73+
"GitHub release workflow must fail clearly when only part of the Android signing secrets are configured",
5974
);
6075

6176
for (const [name, source] of [

desktop/scripts/check-security-governance-config.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ for (const token of [
102102
}
103103
assert.match(releaseSource, /aquasecurity\/trivy-action@/, "release workflow must scan the backend Docker image before publishing");
104104
assert.match(releaseSource, /severity:\s*'CRITICAL,HIGH'/, "release Docker scan must fail on high and critical findings");
105+
assert.match(releaseSource, /ignore-unfixed:\s*true/, "release Docker scan must ignore vulnerabilities that do not have a fixed version");
106+
assert.match(ciSource, /ignore-unfixed:\s*true/, "CI Docker scan must ignore vulnerabilities that do not have a fixed version");
105107

106108
for (const token of [
107109
"SECURITY.md",

docs/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,6 @@ Android 和 Windows 都需要维护:
132132
- 后端所有业务数据按 `user_id` 过滤,任务父子关系也必须校验归属。
133133
- Refresh Token 绑定设备;删除设备会撤销该设备登录态。
134134
- WebSocket 使用短期 Ticket,连接后仍校验 `device_id`
135-
- Android Release 必须使用正式签名,缺少签名配置时构建失败
135+
- Android Release 优先使用正式签名;缺少签名配置时可构建明确标注的 unsigned release APK
136136
- Windows Electron renderer 启用 sandbox,IPC 会校验调用方窗口。
137137
- Docker release 部署默认只暴露 API,MySQL 和 Redis 保持在 Compose 内部网络。

docs/development-roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@
112112
- CI 工作流:后端测试、Docker 构建、桌面端构建、Android 构建。
113113
- Release 工作流:Android APK、Windows 安装包和后端 Docker 镜像。
114114
- GHCR 后端镜像发布。
115-
- Android release 签名强校验,缺少签名配置时发布失败
115+
- Android release 支持正式签名校验;缺少签名配置时发布明确标注的 unsigned APK
116116
- 发布说明文档。
117117
- 安全说明和常见问题文档。
118118
- 后端响应增加 `X-Request-ID`、基础安全头和数据库 readiness 探针。

0 commit comments

Comments
 (0)