fix: harden native release asset workflow
This commit is contained in:
1 parent
fbaa5e7b12
commit
04a3ee477d
8 files changed
+424
-81
No files matched your search
@@ -2,6 +2,11 @@ name: Desktop build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: "Prepare macOS assets on this existing Draft Release (optional)"
|
||||
required: false
|
||||
type: string
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
@@ -10,10 +15,18 @@ permissions:
|
||||
|
||||
jobs:
|
||||
release-plan:
|
||||
if: github.repository == 'tianma-if/edgeever' && github.event_name == 'release'
|
||||
if: >-
|
||||
${{
|
||||
github.repository == 'tianma-if/edgeever' &&
|
||||
(
|
||||
github.event_name == 'release' ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '')
|
||||
)
|
||||
}}
|
||||
name: Plan desktop release asset
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
asset_ready: ${{ steps.plan.outputs.asset_ready }}
|
||||
previous_tag: ${{ steps.plan.outputs.previous_tag }}
|
||||
rebuild: ${{ steps.plan.outputs.rebuild }}
|
||||
steps:
|
||||
@@ -22,11 +35,24 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate Draft Release target
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
run: |
|
||||
expected_version="$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version")"
|
||||
test "$RELEASE_TAG" = "v${expected_version}"
|
||||
test "$(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft --jq '.isDraft')" = "true"
|
||||
target_commitish="$(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json targetCommitish --jq '.targetCommitish')"
|
||||
test "$(git rev-parse "${target_commitish}^{commit}")" = "$(git rev-parse "${GITHUB_SHA}^{commit}")"
|
||||
|
||||
- name: Compare with previous formal release
|
||||
id: plan
|
||||
env:
|
||||
CURRENT_TAG: ${{ github.event.release.tag_name }}
|
||||
CURRENT_TAG: ${{ inputs.release_tag || github.event.release.tag_name }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
HEAD_REF: ${{ github.event_name == 'release' && github.event.release.tag_name || github.sha }}
|
||||
run: |
|
||||
mapfile -t release_tags < <(
|
||||
gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" \
|
||||
@@ -42,15 +68,38 @@ jobs:
|
||||
test -n "$previous_tag"
|
||||
git fetch origin "refs/tags/${previous_tag}:refs/tags/${previous_tag}"
|
||||
echo "previous_tag=$previous_tag" >> "$GITHUB_OUTPUT"
|
||||
node scripts/plan-native-release.mjs desktop "$previous_tag" "$CURRENT_TAG" >> "$GITHUB_OUTPUT"
|
||||
plan_output="$(node scripts/plan-native-release.mjs desktop "$previous_tag" "$HEAD_REF")"
|
||||
printf '%s\n' "$plan_output" >> "$GITHUB_OUTPUT"
|
||||
rebuild="$(printf '%s\n' "$plan_output" | sed -n 's/^rebuild=//p')"
|
||||
expected_version="$(node -p "JSON.parse(require('fs').readFileSync('apps/desktop/package.json', 'utf8')).version")"
|
||||
if [[ "$rebuild" = "true" ]]; then
|
||||
root_version="$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version")"
|
||||
test "$expected_version" = "$root_version"
|
||||
fi
|
||||
|
||||
mapfile -t current_assets < <(
|
||||
gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${CURRENT_TAG}" \
|
||||
--jq '.assets[].name'
|
||||
)
|
||||
asset_ready="$(
|
||||
printf '%s\n' "${current_assets[@]}" |
|
||||
node scripts/check-native-release-assets.mjs desktop "$rebuild" "$CURRENT_TAG" "$expected_version"
|
||||
)"
|
||||
echo "asset_ready=$asset_ready" >> "$GITHUB_OUTPUT"
|
||||
if [[ "$GITHUB_EVENT_NAME" = "release" && "$asset_ready" != "true" ]]; then
|
||||
echo "::error::Published Release assets are incomplete or incompatible. Restore the Release to Draft and prepare assets with workflow_dispatch."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
reuse-release-desktop:
|
||||
needs: release-plan
|
||||
if: >-
|
||||
${{
|
||||
github.repository == 'tianma-if/edgeever' &&
|
||||
github.event_name == 'release' &&
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
inputs.release_tag != '' &&
|
||||
needs.release-plan.result == 'success' &&
|
||||
needs.release-plan.outputs.asset_ready == 'false' &&
|
||||
needs.release-plan.outputs.rebuild == 'false'
|
||||
}}
|
||||
name: Reuse macOS release assets
|
||||
@@ -58,7 +107,7 @@ jobs:
|
||||
steps:
|
||||
- name: Copy latest compatible macOS assets without renaming
|
||||
env:
|
||||
CURRENT_TAG: ${{ github.event.release.tag_name }}
|
||||
CURRENT_TAG: ${{ inputs.release_tag || github.event.release.tag_name }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PREVIOUS_TAG: ${{ needs.release-plan.outputs.previous_tag }}
|
||||
run: |
|
||||
@@ -81,10 +130,12 @@ jobs:
|
||||
always() &&
|
||||
github.repository == 'tianma-if/edgeever' &&
|
||||
(
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.release_tag == '') ||
|
||||
(
|
||||
github.event_name == 'release' &&
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
inputs.release_tag != '' &&
|
||||
needs.release-plan.result == 'success' &&
|
||||
needs.release-plan.outputs.asset_ready == 'false' &&
|
||||
needs.release-plan.outputs.rebuild == 'true'
|
||||
)
|
||||
)
|
||||
@@ -222,15 +273,27 @@ jobs:
|
||||
echo "APPLE_API_ISSUER=$APPLE_API_ISSUER" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Package desktop installer
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: bun scripts/run-desktop-builder.mjs --publish ${{ github.event_name == 'release' && 'always' || 'never' }}
|
||||
run: bun scripts/run-desktop-builder.mjs --publish never
|
||||
|
||||
- name: Verify desktop installer
|
||||
env:
|
||||
EDGE_EVER_VERIFY_TARGET: darwin
|
||||
run: bun scripts/verify-desktop-package.mjs
|
||||
|
||||
- name: Upload verified desktop assets to Release
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
run: |
|
||||
version="$(node -p "JSON.parse(require('fs').readFileSync('apps/desktop/package.json', 'utf8')).version")"
|
||||
gh release upload "$RELEASE_TAG" \
|
||||
"release/desktop/EdgeEver-${version}-mac-arm64.dmg" \
|
||||
"release/desktop/EdgeEver-${version}-mac-arm64.dmg.blockmap" \
|
||||
"release/desktop/latest-mac.yml" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--clobber
|
||||
|
||||
- name: Upload desktop installer and metadata
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
|
||||
@@ -28,10 +28,18 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
release-plan:
|
||||
if: github.repository == 'tianma-if/edgeever' && github.event_name == 'release'
|
||||
if: >-
|
||||
${{
|
||||
github.repository == 'tianma-if/edgeever' &&
|
||||
(
|
||||
github.event_name == 'release' ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '')
|
||||
)
|
||||
}}
|
||||
name: Plan Android release asset
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
asset_ready: ${{ steps.plan.outputs.asset_ready }}
|
||||
previous_tag: ${{ steps.plan.outputs.previous_tag }}
|
||||
rebuild: ${{ steps.plan.outputs.rebuild }}
|
||||
steps:
|
||||
@@ -40,11 +48,24 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate Draft Release target
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
run: |
|
||||
expected_version="$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version")"
|
||||
test "$RELEASE_TAG" = "v${expected_version}"
|
||||
test "$(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft --jq '.isDraft')" = "true"
|
||||
target_commitish="$(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json targetCommitish --jq '.targetCommitish')"
|
||||
test "$(git rev-parse "${target_commitish}^{commit}")" = "$(git rev-parse "${GITHUB_SHA}^{commit}")"
|
||||
|
||||
- name: Compare with previous formal release
|
||||
id: plan
|
||||
env:
|
||||
CURRENT_TAG: ${{ github.event.release.tag_name }}
|
||||
CURRENT_TAG: ${{ inputs.release_tag || github.event.release.tag_name }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
HEAD_REF: ${{ github.event_name == 'release' && github.event.release.tag_name || github.sha }}
|
||||
run: |
|
||||
mapfile -t release_tags < <(
|
||||
gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" \
|
||||
@@ -60,15 +81,51 @@ jobs:
|
||||
test -n "$previous_tag"
|
||||
git fetch origin "refs/tags/${previous_tag}:refs/tags/${previous_tag}"
|
||||
echo "previous_tag=$previous_tag" >> "$GITHUB_OUTPUT"
|
||||
node scripts/plan-native-release.mjs mobile "$previous_tag" "$CURRENT_TAG" >> "$GITHUB_OUTPUT"
|
||||
plan_output="$(node scripts/plan-native-release.mjs mobile "$previous_tag" "$HEAD_REF")"
|
||||
printf '%s\n' "$plan_output" >> "$GITHUB_OUTPUT"
|
||||
rebuild="$(printf '%s\n' "$plan_output" | sed -n 's/^rebuild=//p')"
|
||||
if [[ "$rebuild" = "true" ]]; then
|
||||
root_version="$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version")"
|
||||
mobile_version="$(node -p "JSON.parse(require('fs').readFileSync('apps/mobile/app.json', 'utf8')).expo.version")"
|
||||
if [[ "$mobile_version" != "$root_version" ]]; then
|
||||
echo "::error::Mobile rebuild requires apps/mobile/app.json expo.version ($mobile_version) to match package.json version ($root_version)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
current_version_code="$(node -p "JSON.parse(require('fs').readFileSync('apps/mobile/app.json', 'utf8')).expo.android.versionCode")"
|
||||
previous_version_code="$(
|
||||
git show "${previous_tag}:apps/mobile/app.json" |
|
||||
node -e "const fs = require('fs'); console.log(JSON.parse(fs.readFileSync(0, 'utf8')).expo.android.versionCode)"
|
||||
)"
|
||||
if (( current_version_code <= previous_version_code )); then
|
||||
echo "::error::Mobile rebuild requires android.versionCode ($current_version_code) to be greater than the previous release ($previous_version_code)."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
mapfile -t current_assets < <(
|
||||
gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${CURRENT_TAG}" \
|
||||
--jq '.assets[].name'
|
||||
)
|
||||
asset_ready="$(
|
||||
printf '%s\n' "${current_assets[@]}" |
|
||||
node scripts/check-native-release-assets.mjs mobile "$rebuild" "$CURRENT_TAG"
|
||||
)"
|
||||
echo "asset_ready=$asset_ready" >> "$GITHUB_OUTPUT"
|
||||
if [[ "$GITHUB_EVENT_NAME" = "release" && "$asset_ready" != "true" ]]; then
|
||||
echo "::error::Published Release assets are incomplete or incompatible. Restore the Release to Draft and prepare assets with workflow_dispatch."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
reuse-release-apk:
|
||||
needs: release-plan
|
||||
if: >-
|
||||
${{
|
||||
github.repository == 'tianma-if/edgeever' &&
|
||||
github.event_name == 'release' &&
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
inputs.release_tag != '' &&
|
||||
needs.release-plan.result == 'success' &&
|
||||
needs.release-plan.outputs.asset_ready == 'false' &&
|
||||
needs.release-plan.outputs.rebuild == 'false'
|
||||
}}
|
||||
name: Reuse Android release APK
|
||||
@@ -76,7 +133,7 @@ jobs:
|
||||
steps:
|
||||
- name: Copy latest compatible APK without renaming
|
||||
env:
|
||||
CURRENT_TAG: ${{ github.event.release.tag_name }}
|
||||
CURRENT_TAG: ${{ inputs.release_tag || github.event.release.tag_name }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PREVIOUS_TAG: ${{ needs.release-plan.outputs.previous_tag }}
|
||||
run: |
|
||||
@@ -97,11 +154,13 @@ jobs:
|
||||
always() &&
|
||||
github.repository == 'tianma-if/edgeever' &&
|
||||
(
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.ref == 'refs/heads/main' ||
|
||||
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.release_tag == '') ||
|
||||
(
|
||||
github.event_name == 'release' &&
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
inputs.release_tag != '' &&
|
||||
needs.release-plan.result == 'success' &&
|
||||
needs.release-plan.outputs.asset_ready == 'false' &&
|
||||
needs.release-plan.outputs.rebuild == 'true'
|
||||
)
|
||||
)
|
||||
@@ -139,7 +198,7 @@ jobs:
|
||||
run: bun run build:android:fast
|
||||
|
||||
- name: Restore Android upload keystore
|
||||
if: github.event_name == 'workflow_dispatch' || github.event_name == 'release'
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
run: |
|
||||
@@ -157,7 +216,7 @@ jobs:
|
||||
run: bun run build:android:play
|
||||
|
||||
- name: Build signed release APK for GitHub Release
|
||||
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.release_tag != '')
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
|
||||
env:
|
||||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
ANDROID_KEYSTORE_FILE: ${{ runner.temp }}/edgeever-upload.p12
|
||||
@@ -173,7 +232,7 @@ jobs:
|
||||
working-directory: apps/mobile/android
|
||||
|
||||
- name: Verify release APK signature
|
||||
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.release_tag != '')
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
|
||||
run: |
|
||||
"$ANDROID_SDK_ROOT/build-tools/36.0.0/apksigner" verify --verbose app/build/outputs/apk/release/app-release.apk
|
||||
working-directory: apps/mobile/android
|
||||
@@ -202,10 +261,10 @@ jobs:
|
||||
compression-level: 0
|
||||
|
||||
- name: Upload release APK to GitHub Release
|
||||
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.release_tag != '')
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }}
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
run: |
|
||||
release_asset="$RUNNER_TEMP/edgeever-android-${RELEASE_TAG}-arm64-v8a.apk"
|
||||
cp apps/mobile/android/app/build/outputs/apk/release/app-release.apk "$release_asset"
|
||||
@@ -220,5 +279,5 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Remove temporary upload keystore
|
||||
if: always() && (github.event_name == 'workflow_dispatch' || github.event_name == 'release')
|
||||
if: always() && github.event_name == 'workflow_dispatch'
|
||||
run: rm -f "$RUNNER_TEMP/edgeever-upload.p12"
|
||||
@@ -13,8 +13,8 @@
|
||||
1. **版本号与基线**:使用 `vX.Y.Z` 格式(非 Draft/Prerelease)。递增根目录 `package.json`;若含移动端修改,同步更新 `apps/mobile/app.json` 的 `expo.version` 并递增 `android.versionCode`。上一个实际 Release 为审计基线。
|
||||
2. **跨平台 Release 资产**:每个正式 Release 页面必须同时包含 macOS arm64 DMG 和 Android arm64 APK。若本次未修改对应原生运行时代码、依赖、配置或构建工具,直接复用上一个正式 Release 中已验证的原始资产,保留原文件名与校验和,禁止仅为匹配新版本号而重命名。
|
||||
3. **验证命令**:必须通过 `bun run typecheck`、`bun run typecheck:mobile` 和 `bun run build:web`。
|
||||
4. **原生资产构建与复用**:仅当变更影响移动端运行时代码、共享依赖、原生配置或 Android 构建工具时,执行 `bun run build:android:apk:local` 构建生产签名 APK(签名配置必须位于仓库外 `~/.config/edgeever/android/signing.env`);仅当变更影响 Electron、Rust sidecar、原生依赖、打包配置或桌面构建工具时重新构建 macOS DMG。无对应原生变更时,由 Release 工作流复用最近兼容资产。
|
||||
5. **禁止公开未验收资产**:Release 准备期间必须保持 Draft;只有最终资产完成本节全部验收后才能公开为正式 Release。禁止把关闭签名或公证生成的本地 DMG 上传到公开 Release;此类产物只能用于开发排障。若 CI 只能通过 `published` 事件触发,应在触发后立即转回 Draft,并在最终公开前确认没有重复工作流仍可能覆盖已验收资产。
|
||||
4. **原生资产构建与复用**:由 `scripts/plan-native-release.mjs` 决定重建或复用;桌面资产包含 `apps/web`。修改判定规则时同步更新测试。移动端重建使用 `bun run build:android:apk:local`,签名配置保存在仓库外。
|
||||
5. **Draft 内准备资产**:通过带 `release_tag` 的 `workflow_dispatch` 在 Draft 中准备并验证资产;`published` 事件只审计,禁止重新构建或上传。
|
||||
6. **macOS 最终资产验收**:验收对象必须是从 GitHub Release 页面重新下载的最终 DMG,禁止用本地构建目录中的产物代替。必须核对 Release digest/本地 SHA-256,并依次通过 `hdiutil verify`、挂载、`codesign --verify --deep --strict`、`spctl --assess --type execute`(结果必须为 `accepted` 且来源为 `Notarized Developer ID`)和 `xcrun stapler validate`。随后必须写入模拟浏览器下载的 `com.apple.quarantine` 属性,将 App 复制到临时目录并实际启动;任何一步失败都禁止发布,也禁止指导用户绕过 Gatekeeper。
|
||||
7. **桌面端真实首启冒烟测试**:凡涉及 Electron、preload、认证、实例地址、首次引导或本地配置,必须对最终打包 App 使用全新的临时 `--user-data-dir` 实际启动,不能复用开发者机器上的历史配置。测试至少覆盖 preload 桥接可用、未配置实例时实例地址为空、首次打开显示实例地址输入界面,以及保存后能进入预期流程。类型检查、Web 构建、ASAR 文件存在性检查和已有用户配置下的启动均不能替代该测试。
|
||||
8. **桌面包结构门禁**:桌面变更除通用验证命令外,必须运行桌面测试及 `EDGE_EVER_VERIFY_TARGET=darwin bun run verify:desktop-package`。验证脚本必须实际加载打包后的 preload 入口并确认桥接 API 可注册;仅检查文件存在或扩展名不视为通过。
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"build:desktop": "EDGE_EVER_DESKTOP_BUILD=1 bun run build:web && bun run build:desktop:sidecar && bun run --cwd apps/desktop dist",
|
||||
"benchmark:desktop:sidecar": "bun scripts/benchmark-desktop-sidecar.mjs",
|
||||
"verify:web-performance": "bun scripts/verify-web-performance.mjs",
|
||||
"test:release-planning": "bun test scripts/plan-native-release.test.mjs scripts/check-native-release-assets.test.mjs",
|
||||
"test:desktop:sidecar": "bun scripts/test-desktop-sidecar.mjs",
|
||||
"test:desktop": "bun test apps/desktop/src/main/account-scope.test.mjs apps/desktop/src/main/diagnostic-log.test.mjs apps/desktop/src/main/file-association.test.mjs apps/desktop/src/main/file-permissions.test.mjs apps/desktop/src/main/resource-url.test.mjs apps/desktop/src/main/staged-resource.test.mjs apps/web/src/lib/api.test.mjs apps/web/src/lib/desktop-resources.test.mjs apps/web/src/lib/desktop-sync.test.mjs apps/web/src/lib/local-mirror.test.mjs apps/web/src/lib/repository.test.mjs apps/web/src/lib/sync-queue.test.mjs && bun run test:desktop:sidecar",
|
||||
"verify:desktop-package": "bun scripts/verify-desktop-package.mjs",
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export const nativeReleaseAssetsReady = ({
|
||||
platform,
|
||||
rebuild,
|
||||
currentTag,
|
||||
desktopVersion,
|
||||
assetNames,
|
||||
}) => {
|
||||
if (platform === "mobile") {
|
||||
const apkNames = assetNames.filter((name) =>
|
||||
/^edgeever-android-v.*-arm64-v8a\.apk$/.test(name),
|
||||
);
|
||||
if (apkNames.length !== 1) return false;
|
||||
return (
|
||||
!rebuild ||
|
||||
apkNames[0] === `edgeever-android-${currentTag}-arm64-v8a.apk`
|
||||
);
|
||||
}
|
||||
|
||||
if (platform === "desktop") {
|
||||
const dmgNames = assetNames.filter((name) =>
|
||||
/^EdgeEver-.*-mac-arm64\.dmg$/.test(name),
|
||||
);
|
||||
const blockmapNames = assetNames.filter((name) =>
|
||||
/^EdgeEver-.*-mac-arm64\.dmg\.blockmap$/.test(name),
|
||||
);
|
||||
if (
|
||||
dmgNames.length !== 1 ||
|
||||
blockmapNames.length !== 1 ||
|
||||
assetNames.filter((name) => name === "latest-mac.yml").length !== 1 ||
|
||||
blockmapNames[0] !== `${dmgNames[0]}.blockmap`
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
!rebuild ||
|
||||
dmgNames[0] === `EdgeEver-${desktopVersion}-mac-arm64.dmg`
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported native release platform: ${platform}`);
|
||||
};
|
||||
|
||||
const run = () => {
|
||||
const [platform, rebuildValue, currentTag, desktopVersion = ""] =
|
||||
process.argv.slice(2);
|
||||
if (
|
||||
!["mobile", "desktop"].includes(platform) ||
|
||||
!["true", "false"].includes(rebuildValue) ||
|
||||
!currentTag
|
||||
) {
|
||||
console.error(
|
||||
"Usage: node scripts/check-native-release-assets.mjs <mobile|desktop> <true|false> <current-tag> [desktop-version]",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const assetNames = readFileSync(0, "utf8").split("\n").filter(Boolean);
|
||||
process.stdout.write(
|
||||
String(
|
||||
nativeReleaseAssetsReady({
|
||||
platform,
|
||||
rebuild: rebuildValue === "true",
|
||||
currentTag,
|
||||
desktopVersion,
|
||||
assetNames,
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
if (
|
||||
process.argv[1] &&
|
||||
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
||||
) {
|
||||
run();
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { nativeReleaseAssetsReady } from "./check-native-release-assets.mjs";
|
||||
|
||||
describe("native release asset readiness", () => {
|
||||
test("accepts one reused Android APK but requires the current name after a rebuild", () => {
|
||||
const reused = ["edgeever-android-v1.6.30-arm64-v8a.apk"];
|
||||
expect(
|
||||
nativeReleaseAssetsReady({
|
||||
platform: "mobile",
|
||||
rebuild: false,
|
||||
currentTag: "v1.6.35",
|
||||
assetNames: reused,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
nativeReleaseAssetsReady({
|
||||
platform: "mobile",
|
||||
rebuild: true,
|
||||
currentTag: "v1.6.35",
|
||||
assetNames: reused,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("rejects duplicate Android APKs", () => {
|
||||
expect(
|
||||
nativeReleaseAssetsReady({
|
||||
platform: "mobile",
|
||||
rebuild: false,
|
||||
currentTag: "v1.6.35",
|
||||
assetNames: [
|
||||
"edgeever-android-v1.6.30-arm64-v8a.apk",
|
||||
"edgeever-android-v1.6.35-arm64-v8a.apk",
|
||||
],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("accepts a matching reused desktop asset set", () => {
|
||||
expect(
|
||||
nativeReleaseAssetsReady({
|
||||
platform: "desktop",
|
||||
rebuild: false,
|
||||
currentTag: "v1.6.35",
|
||||
desktopVersion: "1.6.35",
|
||||
assetNames: [
|
||||
"EdgeEver-1.6.33-mac-arm64.dmg",
|
||||
"EdgeEver-1.6.33-mac-arm64.dmg.blockmap",
|
||||
"latest-mac.yml",
|
||||
],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("requires current, complete desktop assets after a rebuild", () => {
|
||||
const current = [
|
||||
"EdgeEver-1.6.35-mac-arm64.dmg",
|
||||
"EdgeEver-1.6.35-mac-arm64.dmg.blockmap",
|
||||
"latest-mac.yml",
|
||||
];
|
||||
expect(
|
||||
nativeReleaseAssetsReady({
|
||||
platform: "desktop",
|
||||
rebuild: true,
|
||||
currentTag: "v1.6.35",
|
||||
desktopVersion: "1.6.35",
|
||||
assetNames: current,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
nativeReleaseAssetsReady({
|
||||
platform: "desktop",
|
||||
rebuild: true,
|
||||
currentTag: "v1.6.35",
|
||||
desktopVersion: "1.6.35",
|
||||
assetNames: current.slice(0, 2),
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,68 +1,82 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const [platform, baseRef, headRef] = process.argv.slice(2);
|
||||
export const planNativeRelease = (platform, changedFiles) => {
|
||||
if (!["mobile", "desktop"].includes(platform)) {
|
||||
throw new Error(`Unsupported native release platform: ${platform}`);
|
||||
}
|
||||
|
||||
if (!["mobile", "desktop"].includes(platform) || !baseRef || !headRef) {
|
||||
console.error(
|
||||
"Usage: node scripts/plan-native-release.mjs <mobile|desktop> <base-ref> <head-ref>",
|
||||
const runtimeChangedFiles = changedFiles.filter(
|
||||
(file) => !file.endsWith(".md"),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const git = (...args) =>
|
||||
execFileSync("git", args, {
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
const relevantPrefixes =
|
||||
platform === "mobile"
|
||||
? ["apps/mobile/", "packages/client/", "packages/shared/"]
|
||||
: [
|
||||
"apps/desktop/",
|
||||
"apps/web/",
|
||||
"crates/desktop-sidecar/",
|
||||
"packages/client/",
|
||||
"packages/shared/",
|
||||
];
|
||||
|
||||
const changedFiles = git("diff", "--name-only", `${baseRef}...${headRef}`)
|
||||
.split("\n")
|
||||
.filter(Boolean);
|
||||
const runtimeChangedFiles = changedFiles.filter(
|
||||
(file) => !file.endsWith(".md"),
|
||||
);
|
||||
const relevantFiles =
|
||||
platform === "mobile"
|
||||
? new Set(["bun.lock", "scripts/build-android-local.sh"])
|
||||
: new Set([
|
||||
"bun.lock",
|
||||
"scripts/run-desktop-builder.mjs",
|
||||
"scripts/verify-desktop-package.mjs",
|
||||
]);
|
||||
|
||||
const packageJsonChangedBeyondVersion = () => {
|
||||
if (!changedFiles.includes("package.json")) return false;
|
||||
const relevantChanges = runtimeChangedFiles.filter(
|
||||
(file) =>
|
||||
relevantPrefixes.some((prefix) => file.startsWith(prefix)) ||
|
||||
relevantFiles.has(file),
|
||||
);
|
||||
|
||||
const readPackage = (ref) => {
|
||||
const packageJson = JSON.parse(git("show", `${ref}:package.json`));
|
||||
delete packageJson.version;
|
||||
return packageJson;
|
||||
return {
|
||||
rebuild: relevantChanges.length > 0,
|
||||
relevantChanges,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
JSON.stringify(readPackage(baseRef)) !==
|
||||
JSON.stringify(readPackage(headRef))
|
||||
const run = () => {
|
||||
const [platform, baseRef, headRef] = process.argv.slice(2);
|
||||
|
||||
if (!["mobile", "desktop"].includes(platform) || !baseRef || !headRef) {
|
||||
console.error(
|
||||
"Usage: node scripts/plan-native-release.mjs <mobile|desktop> <base-ref> <head-ref>",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const git = (...args) =>
|
||||
execFileSync("git", args, {
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
|
||||
const changedFiles = git("diff", "--name-only", `${baseRef}...${headRef}`)
|
||||
.split("\n")
|
||||
.filter(Boolean);
|
||||
const { rebuild, relevantChanges } = planNativeRelease(
|
||||
platform,
|
||||
changedFiles,
|
||||
);
|
||||
|
||||
process.stdout.write(`rebuild=${rebuild}\n`);
|
||||
process.stderr.write(
|
||||
`${platform} release plan: ${rebuild ? "rebuild" : "reuse"}${
|
||||
relevantChanges.length > 0 ? ` (${relevantChanges.join(", ")})` : ""
|
||||
}\n`,
|
||||
);
|
||||
};
|
||||
|
||||
const relevantPrefixes =
|
||||
platform === "mobile"
|
||||
? ["apps/mobile/", "packages/client/", "packages/shared/"]
|
||||
: ["apps/desktop/", "crates/desktop-sidecar/"];
|
||||
|
||||
const relevantFiles =
|
||||
platform === "mobile"
|
||||
? new Set(["bun.lock", "scripts/build-android-local.sh"])
|
||||
: new Set([
|
||||
"scripts/run-desktop-builder.mjs",
|
||||
"scripts/verify-desktop-package.mjs",
|
||||
]);
|
||||
|
||||
const relevantChanges = runtimeChangedFiles.filter(
|
||||
(file) =>
|
||||
relevantPrefixes.some((prefix) => file.startsWith(prefix)) ||
|
||||
relevantFiles.has(file) ||
|
||||
(platform === "mobile" &&
|
||||
file === "package.json" &&
|
||||
packageJsonChangedBeyondVersion()),
|
||||
);
|
||||
|
||||
const rebuild = relevantChanges.length > 0;
|
||||
|
||||
process.stdout.write(`rebuild=${rebuild}\n`);
|
||||
process.stderr.write(
|
||||
`${platform} release plan: ${rebuild ? "rebuild" : "reuse"}${
|
||||
relevantChanges.length > 0 ? ` (${relevantChanges.join(", ")})` : ""
|
||||
}\n`,
|
||||
);
|
||||
if (
|
||||
process.argv[1] &&
|
||||
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
||||
) {
|
||||
run();
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { planNativeRelease } from "./plan-native-release.mjs";
|
||||
|
||||
describe("native release planning", () => {
|
||||
test("does not rebuild Android for root scripts, versions, or documentation", () => {
|
||||
expect(
|
||||
planNativeRelease("mobile", [
|
||||
"package.json",
|
||||
"AGENTS.md",
|
||||
"apps/web/src/app/App.tsx",
|
||||
"apps/desktop/package.json",
|
||||
]),
|
||||
).toEqual({ rebuild: false, relevantChanges: [] });
|
||||
});
|
||||
|
||||
test("rebuilds Android for mobile, shared runtime, dependency, or build changes", () => {
|
||||
const changedFiles = [
|
||||
"apps/mobile/src/screens/LoginScreen.tsx",
|
||||
"packages/shared/src/index.ts",
|
||||
"bun.lock",
|
||||
"scripts/build-android-local.sh",
|
||||
];
|
||||
expect(planNativeRelease("mobile", changedFiles)).toEqual({
|
||||
rebuild: true,
|
||||
relevantChanges: changedFiles,
|
||||
});
|
||||
});
|
||||
|
||||
test("rebuilds desktop for its embedded Web renderer and shared runtime", () => {
|
||||
const changedFiles = [
|
||||
"apps/web/src/app/App.tsx",
|
||||
"apps/desktop/src/main/index.mjs",
|
||||
"packages/shared/src/index.ts",
|
||||
];
|
||||
expect(planNativeRelease("desktop", changedFiles)).toEqual({
|
||||
rebuild: true,
|
||||
relevantChanges: changedFiles,
|
||||
});
|
||||
});
|
||||
|
||||
test("does not rebuild desktop for release notes or a root version bump alone", () => {
|
||||
expect(
|
||||
planNativeRelease("desktop", ["package.json", "AGENTS.md"]),
|
||||
).toEqual({ rebuild: false, relevantChanges: [] });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user