feat: separate store delivery from releases
This commit is contained in:
1 parent
313c3dab64
commit
c941dcf032
14 files changed
+876
-70
No files matched your search
@@ -4,8 +4,8 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: "Upload the signed arm64 APK to this existing GitHub Release (optional)"
|
||||
required: false
|
||||
description: "Upload the signed arm64 APK to this existing Draft GitHub Release"
|
||||
required: true
|
||||
type: string
|
||||
release:
|
||||
types: [published]
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
github.repository == 'tianma-if/edgeever' &&
|
||||
(
|
||||
github.event_name == 'release' ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '')
|
||||
github.event_name == 'workflow_dispatch'
|
||||
)
|
||||
}}
|
||||
name: Plan Android release asset
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate Draft Release target
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
@@ -123,7 +123,6 @@ jobs:
|
||||
${{
|
||||
github.repository == 'tianma-if/edgeever' &&
|
||||
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'
|
||||
@@ -155,10 +154,8 @@ jobs:
|
||||
github.repository == 'tianma-if/edgeever' &&
|
||||
(
|
||||
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.release_tag == '') ||
|
||||
(
|
||||
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'
|
||||
@@ -206,17 +203,8 @@ jobs:
|
||||
printf '%s' "$ANDROID_KEYSTORE_BASE64" | openssl base64 -d -A > "$RUNNER_TEMP/edgeever-upload.p12"
|
||||
chmod 600 "$RUNNER_TEMP/edgeever-upload.p12"
|
||||
|
||||
- name: Build signed release AAB
|
||||
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
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
run: bun run build:android:play
|
||||
|
||||
- name: Build signed release APK for GitHub Release
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
ANDROID_KEYSTORE_FILE: ${{ runner.temp }}/edgeever-upload.p12
|
||||
@@ -225,23 +213,12 @@ jobs:
|
||||
EDGE_EVER_ANDROID_APK_ARCHS: arm64-v8a
|
||||
run: bun run build:android:apk
|
||||
|
||||
- name: Verify release AAB signature
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release_tag == ''
|
||||
run: |
|
||||
"$JAVA_HOME/bin/jarsigner" -verify app/build/outputs/bundle/release/*.aab
|
||||
working-directory: apps/mobile/android
|
||||
|
||||
- name: Verify release APK signature
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
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
|
||||
|
||||
- name: Verify R8 deobfuscation mapping
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release_tag == ''
|
||||
run: test -s app/build/outputs/mapping/release/mapping.txt
|
||||
working-directory: apps/mobile/android
|
||||
|
||||
- name: Upload APK
|
||||
if: github.event_name == 'push'
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -251,17 +228,8 @@ jobs:
|
||||
if-no-files-found: error
|
||||
compression-level: 0
|
||||
|
||||
- name: Upload release AAB
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release_tag == ''
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: edgeever-android-release-aab
|
||||
path: apps/mobile/android/app/build/outputs/bundle/release/*.aab
|
||||
if-no-files-found: error
|
||||
compression-level: 0
|
||||
|
||||
- name: Upload release APK to GitHub Release
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
@@ -270,14 +238,6 @@ jobs:
|
||||
cp apps/mobile/android/app/build/outputs/apk/release/app-release.apk "$release_asset"
|
||||
gh release upload "$RELEASE_TAG" "$release_asset" --repo "$GITHUB_REPOSITORY" --clobber
|
||||
|
||||
- name: Upload R8 deobfuscation mapping
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release_tag == ''
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: edgeever-android-release-mapping
|
||||
path: apps/mobile/android/app/build/outputs/mapping/release/mapping.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Remove temporary upload keystore
|
||||
if: always() && github.event_name == 'workflow_dispatch'
|
||||
run: rm -f "$RUNNER_TEMP/edgeever-upload.p12"
|
||||
@@ -0,0 +1,231 @@
|
||||
name: Deliver Mobile Stores
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: "Formal GitHub Release tag to deliver (vX.Y.Z)"
|
||||
required: true
|
||||
type: string
|
||||
platform:
|
||||
description: "Store platform"
|
||||
required: true
|
||||
default: both
|
||||
type: choice
|
||||
options:
|
||||
- both
|
||||
- android
|
||||
- ios
|
||||
android_track:
|
||||
description: "Google Play track (production is uploaded as a Draft)"
|
||||
required: true
|
||||
default: internal
|
||||
type: choice
|
||||
options:
|
||||
- internal
|
||||
- alpha
|
||||
- beta
|
||||
- production
|
||||
production_confirmation:
|
||||
description: "For Google Play production, type the exact release tag"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: edgeever-store-delivery-${{ inputs.release_tag }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
plan:
|
||||
if: github.repository == 'tianma-if/edgeever'
|
||||
name: Validate store delivery source
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.validate.outputs.version }}
|
||||
version_code: ${{ steps.validate.outputs.version_code }}
|
||||
steps:
|
||||
- name: Check out the immutable Release tag
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ inputs.release_tag }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate formal Release and resolve its predecessor
|
||||
id: release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
run: |
|
||||
release="$(
|
||||
gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}"
|
||||
)"
|
||||
test "$(jq -r '.draft' <<<"$release")" = "false"
|
||||
test "$(jq -r '.prerelease' <<<"$release")" = "false"
|
||||
test "$(jq -r '.tag_name' <<<"$release")" = "$RELEASE_TAG"
|
||||
|
||||
release_target="$(jq -r '.target_commitish' <<<"$release")"
|
||||
test "$(git rev-parse "${RELEASE_TAG}^{commit}")" = "$(git rev-parse "${release_target}^{commit}")"
|
||||
|
||||
mapfile -t release_tags < <(
|
||||
gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" \
|
||||
--jq '.[] | select(.draft == false and .prerelease == false) | .tag_name'
|
||||
)
|
||||
previous_tag=""
|
||||
current_found="false"
|
||||
for candidate in "${release_tags[@]}"; do
|
||||
if [[ "$current_found" = "true" ]]; then
|
||||
previous_tag="$candidate"
|
||||
break
|
||||
fi
|
||||
if [[ "$candidate" = "$RELEASE_TAG" ]]; then
|
||||
current_found="true"
|
||||
fi
|
||||
done
|
||||
test "$current_found" = "true"
|
||||
test -n "$previous_tag"
|
||||
git fetch origin "refs/tags/${previous_tag}:refs/tags/${previous_tag}"
|
||||
echo "previous_tag=$previous_tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate mobile version and audited change range
|
||||
id: validate
|
||||
env:
|
||||
ANDROID_TRACK: ${{ inputs.android_track }}
|
||||
PLATFORM: ${{ inputs.platform }}
|
||||
PREVIOUS_TAG: ${{ steps.release.outputs.previous_tag }}
|
||||
PRODUCTION_CONFIRMATION: ${{ inputs.production_confirmation }}
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
run: |
|
||||
node scripts/validate-store-delivery.mjs \
|
||||
"$RELEASE_TAG" \
|
||||
"$PREVIOUS_TAG" \
|
||||
"$PLATFORM" \
|
||||
"$ANDROID_TRACK" \
|
||||
"$PRODUCTION_CONFIRMATION" |
|
||||
tee -a "$GITHUB_OUTPUT"
|
||||
|
||||
android:
|
||||
needs: plan
|
||||
if: inputs.platform == 'android' || inputs.platform == 'both'
|
||||
name: Deliver Google Play
|
||||
runs-on: [self-hosted, macOS, ARM64, edgeever-mobile]
|
||||
timeout-minutes: 60
|
||||
environment: ${{ inputs.android_track == 'production' && 'store-production' || 'store-delivery' }}
|
||||
env:
|
||||
ANDROID_HOME: /opt/homebrew/share/android-commandlinetools
|
||||
ANDROID_SDK_ROOT: /opt/homebrew/share/android-commandlinetools
|
||||
JAVA_HOME: /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
|
||||
steps:
|
||||
- name: Check out the immutable Release tag
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ inputs.release_tag }}
|
||||
clean: true
|
||||
|
||||
- name: Set up Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.14
|
||||
|
||||
- name: Restore Android upload keystore
|
||||
env:
|
||||
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
run: |
|
||||
test -n "$ANDROID_KEYSTORE_BASE64"
|
||||
printf '%s' "$ANDROID_KEYSTORE_BASE64" | openssl base64 -d -A > "$RUNNER_TEMP/edgeever-upload.p12"
|
||||
chmod 600 "$RUNNER_TEMP/edgeever-upload.p12"
|
||||
|
||||
- name: Build and verify signed Play bundle
|
||||
env:
|
||||
ANDROID_KEYSTORE_FILE: ${{ runner.temp }}/edgeever-upload.p12
|
||||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
run: bun run build:android:play
|
||||
|
||||
- name: Preserve Play bundle and deobfuscation mapping
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: edgeever-${{ inputs.release_tag }}-google-play
|
||||
path: |
|
||||
apps/mobile/android/app/build/outputs/bundle/release/app-release.aab
|
||||
apps/mobile/android/app/build/outputs/mapping/release/mapping.txt
|
||||
if-no-files-found: error
|
||||
compression-level: 0
|
||||
|
||||
- name: Upload bundle to Google Play
|
||||
env:
|
||||
ANDROID_TRACK: ${{ inputs.android_track }}
|
||||
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
|
||||
run: |
|
||||
test -n "$EXPO_TOKEN"
|
||||
bunx eas-cli submit \
|
||||
--platform android \
|
||||
--profile "store-${ANDROID_TRACK}" \
|
||||
--path android/app/build/outputs/bundle/release/app-release.aab \
|
||||
--non-interactive \
|
||||
--wait
|
||||
working-directory: apps/mobile
|
||||
|
||||
- name: Remove temporary upload keystore
|
||||
if: always()
|
||||
run: rm -f "$RUNNER_TEMP/edgeever-upload.p12"
|
||||
|
||||
ios:
|
||||
needs: plan
|
||||
if: inputs.platform == 'ios' || inputs.platform == 'both'
|
||||
name: Deliver App Store Connect
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
environment: store-delivery
|
||||
steps:
|
||||
- name: Check out the immutable Release tag
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ inputs.release_tag }}
|
||||
|
||||
- name: Set up Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.14
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Build the iOS archive on EAS
|
||||
id: build
|
||||
env:
|
||||
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
run: |
|
||||
test -n "$EXPO_TOKEN"
|
||||
build_json="$(
|
||||
bunx eas-cli build \
|
||||
--platform ios \
|
||||
--profile production \
|
||||
--message "Store delivery ${RELEASE_TAG}" \
|
||||
--freeze-credentials \
|
||||
--non-interactive \
|
||||
--wait \
|
||||
--json
|
||||
)"
|
||||
build_id="$(jq -r 'if type == "array" then .[0].id else .id end' <<<"$build_json")"
|
||||
test -n "$build_id"
|
||||
test "$build_id" != "null"
|
||||
echo "build_id=$build_id" >> "$GITHUB_OUTPUT"
|
||||
working-directory: apps/mobile
|
||||
|
||||
- name: Upload archive to App Store Connect and TestFlight
|
||||
env:
|
||||
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
|
||||
run: |
|
||||
bunx eas-cli submit \
|
||||
--platform ios \
|
||||
--profile store-ios \
|
||||
--id "${{ steps.build.outputs.build_id }}" \
|
||||
--non-interactive \
|
||||
--wait
|
||||
working-directory: apps/mobile
|
||||
@@ -25,6 +25,36 @@
|
||||
"ios": {
|
||||
"ascAppId": "6792625631"
|
||||
}
|
||||
},
|
||||
"store-internal": {
|
||||
"android": {
|
||||
"track": "internal",
|
||||
"releaseStatus": "completed"
|
||||
}
|
||||
},
|
||||
"store-alpha": {
|
||||
"android": {
|
||||
"track": "alpha",
|
||||
"releaseStatus": "completed"
|
||||
}
|
||||
},
|
||||
"store-beta": {
|
||||
"android": {
|
||||
"track": "beta",
|
||||
"releaseStatus": "completed"
|
||||
}
|
||||
},
|
||||
"store-production": {
|
||||
"android": {
|
||||
"track": "production",
|
||||
"releaseStatus": "draft",
|
||||
"changesNotSentForReview": true
|
||||
}
|
||||
},
|
||||
"store-ios": {
|
||||
"ios": {
|
||||
"ascAppId": "6792625631"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-22
@@ -86,26 +86,18 @@ app unless the key reset has been completed in Play Console.
|
||||
|
||||
Set `EDGE_EVER_ANDROID_ENV_FILE` to use a different secure environment file.
|
||||
|
||||
### GitHub Actions fallback
|
||||
### Automated store delivery
|
||||
|
||||
Run the workflow manually to build a signed Android App Bundle. The workflow
|
||||
uses the following GitHub Actions secrets:
|
||||
The GitHub Release workflow never builds or uploads a Play bundle. After a
|
||||
formal Release that contains mobile changes is published, use the separate
|
||||
store-delivery workflow. It builds a signed AAB from the immutable Release tag,
|
||||
preserves the AAB and R8 mapping as Actions artifacts, and uploads the bundle
|
||||
through EAS Submit. See [Mobile Store Delivery](store-delivery.md).
|
||||
|
||||
```text
|
||||
ANDROID_KEYSTORE_BASE64
|
||||
ANDROID_KEYSTORE_PASSWORD
|
||||
ANDROID_KEY_ALIAS
|
||||
ANDROID_KEY_PASSWORD
|
||||
```
|
||||
|
||||
The resulting app bundle is uploaded as `edgeever-android-release-aab`. Release
|
||||
builds enable R8 code minification and resource shrinking, and the matching
|
||||
deobfuscation file is uploaded as `edgeever-android-release-mapping`. Upload
|
||||
that `mapping.txt` alongside the same app bundle version in Google Play Console
|
||||
so production crash and ANR stack traces can be decoded correctly. The upload
|
||||
keystore is only used to prove ownership when uploading bundles; Google Play
|
||||
App Signing manages the app signing key delivered to users. Keep an encrypted
|
||||
backup of the upload keystore and its credentials outside the repository.
|
||||
The upload keystore is only used to prove ownership when uploading bundles;
|
||||
Google Play App Signing manages the app signing key delivered to users. Keep an
|
||||
encrypted backup of the upload keystore and its credentials outside the
|
||||
repository.
|
||||
|
||||
### iOS App Store build
|
||||
|
||||
@@ -122,10 +114,9 @@ bunx eas-cli build --platform ios --profile production
|
||||
|
||||
The first command requires the Apple Account Holder to authenticate and may
|
||||
prompt for two-factor authentication. The production profile automatically
|
||||
increments the App Store build number. After the build has succeeded, submit
|
||||
the selected build with `bunx eas-cli submit --platform ios --profile
|
||||
production`, or configure `submit.production.ios.ascAppId` and use
|
||||
`--auto-submit` on subsequent releases. Apple credentials, App Store Connect
|
||||
increments the App Store build number. Routine delivery should use the separate
|
||||
store-delivery workflow, which builds from an immutable formal Release tag and
|
||||
uploads to App Store Connect/TestFlight. Apple credentials, App Store Connect
|
||||
API keys, certificates, and provisioning profiles must never be committed.
|
||||
|
||||
## EAS
|
||||
|
||||
@@ -5,6 +5,10 @@ the repository, creates the tracking Issue, updates versions, prepares and audit
|
||||
native assets in a Draft Release, publishes the Release, and installs the final
|
||||
macOS DMG.
|
||||
|
||||
This process does not perform mobile store delivery. Google Play and App Store
|
||||
Connect delivery is a separate, explicit operation documented in
|
||||
[Mobile Store Delivery](store-delivery.md).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Run the command on macOS from a clean `main` branch that exactly matches
|
||||
@@ -51,6 +55,9 @@ maintainer releases should install and launch the published application.
|
||||
8. Link and close the tracking Issue, download the final DMG, verify its checksum
|
||||
and signature, replace `/Applications/EdgeEver.app`, and launch it.
|
||||
|
||||
No Release step builds a Play AAB, starts an EAS iOS build, or uploads to a
|
||||
mobile store.
|
||||
|
||||
The release version commit includes the standard GitHub Actions skip marker.
|
||||
Draft asset workflows are dispatched explicitly, so this avoids an unnecessary
|
||||
push-triggered Android build without skipping required release builds.
|
||||
|
||||
@@ -4,6 +4,9 @@ EdgeEver 使用一条本地命令准备 Release。该命令会检查仓库、创
|
||||
更新版本、在 Draft Release 中准备并审计原生资产、正式发布 Release,并安装最终
|
||||
的 macOS DMG。
|
||||
|
||||
该流程不执行移动端商店交付。Google Play 和 App Store Connect 交付是独立且
|
||||
需要显式触发的操作,详见[移动端商店交付](store-delivery.zh-CN.md)。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 在 macOS 上执行,当前必须是干净的 `main` 分支,并且与 `origin/main` 完全一致。
|
||||
@@ -45,6 +48,8 @@ GitHub 状态。`--skip-install` 会跳过发布后的 DMG 安装,仅适用于
|
||||
8. 回链并关闭跟踪 Issue,下载最终 DMG,验证校验和与签名,覆盖
|
||||
`/Applications/EdgeEver.app` 并启动。
|
||||
|
||||
Release 流程不会构建 Play AAB、启动 EAS iOS Build,也不会上传到移动端商店。
|
||||
|
||||
版本发布提交会包含 GitHub Actions 的标准跳过标记。Draft 资产工作流由脚本
|
||||
显式触发,因此可以避免一次不必要的 push 触发 Android 构建,同时不会跳过
|
||||
正式发布所需的构建。
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# Mobile Store Delivery
|
||||
|
||||
GitHub Releases and mobile store delivery are separate operations:
|
||||
|
||||
- `bun run release` creates and audits the GitHub Release. It never contacts
|
||||
Google Play or App Store Connect.
|
||||
- `bun run publish:stores` dispatches a manual store-delivery workflow for one
|
||||
existing formal Release tag.
|
||||
- Store publication remains a separate decision. Google Play production uploads
|
||||
remain Drafts, and iOS uploads stop at App Store Connect/TestFlight.
|
||||
|
||||
## Safety Model
|
||||
|
||||
The workflow checks out the immutable Release tag rather than `main`. Before any
|
||||
store build starts, it verifies that:
|
||||
|
||||
- the tag belongs to a formal, non-prerelease GitHub Release;
|
||||
- the Release target and Git tag resolve to the same commit;
|
||||
- the audited range since the previous formal Release contains mobile runtime
|
||||
changes;
|
||||
- root and mobile app versions both match the Release tag;
|
||||
- Android `versionCode` increased;
|
||||
- a Google Play production request repeats the exact Release tag as confirmation.
|
||||
|
||||
A Release that reused the previous mobile binary is intentionally rejected. It
|
||||
does not represent a new store binary and should not be uploaded again.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Configure these GitHub repository secrets:
|
||||
|
||||
- `EXPO_TOKEN`
|
||||
- `ANDROID_KEYSTORE_BASE64`
|
||||
- `ANDROID_KEYSTORE_PASSWORD`
|
||||
- `ANDROID_KEY_ALIAS`
|
||||
- `ANDROID_KEY_PASSWORD`
|
||||
|
||||
Upload the Google Play service-account key to the Android application's EAS
|
||||
Submit credentials. Configure the iOS distribution credentials and App Store
|
||||
Connect API key in EAS. Credentials and private keys must remain outside the
|
||||
repository.
|
||||
|
||||
Create these GitHub Environments:
|
||||
|
||||
- `store-delivery` for Internal/Alpha/Beta and TestFlight delivery.
|
||||
- `store-production` for Google Play production delivery. Configure required
|
||||
reviewers on this environment.
|
||||
|
||||
EAS Submit requires the application to exist in each store and Google Play API
|
||||
submission requires the service account to have access to the application.
|
||||
Follow the official
|
||||
[EAS Android submission guide](https://docs.expo.dev/submit/android/) and
|
||||
[EAS Submit configuration reference](https://docs.expo.dev/submit/eas-json/).
|
||||
|
||||
## Commands
|
||||
|
||||
Deliver both platforms to Google Play Internal and App Store Connect/TestFlight:
|
||||
|
||||
```sh
|
||||
bun run publish:stores -- --release v1.7.0
|
||||
```
|
||||
|
||||
Deliver only Android to a closed testing track:
|
||||
|
||||
```sh
|
||||
bun run publish:stores -- \
|
||||
--release v1.7.0 \
|
||||
--platform android \
|
||||
--android-track beta
|
||||
```
|
||||
|
||||
Prepare a Google Play production Draft:
|
||||
|
||||
```sh
|
||||
bun run publish:stores -- \
|
||||
--release v1.7.0 \
|
||||
--platform android \
|
||||
--android-track production \
|
||||
--confirm-production v1.7.0
|
||||
```
|
||||
|
||||
Use `--dry-run` to print the GitHub workflow dispatch without starting it.
|
||||
|
||||
## Platform Behavior
|
||||
|
||||
### Google Play
|
||||
|
||||
The self-hosted release runner builds a signed AAB from the selected tag,
|
||||
verifies its signature and R8 mapping, preserves both as GitHub Actions
|
||||
artifacts, and uploads the AAB through EAS Submit.
|
||||
|
||||
Internal, Alpha, and Beta profiles use a completed release on the selected
|
||||
testing track. Production uses `releaseStatus: draft` and
|
||||
`changesNotSentForReview: true`; completing review and rollout remains a manual
|
||||
Google Play Console action.
|
||||
|
||||
### App Store Connect
|
||||
|
||||
EAS Build creates the signed iOS archive from the selected tag and increments
|
||||
the remote iOS build number. EAS Submit uploads that exact build to App Store
|
||||
Connect, where it becomes available to TestFlight after Apple finishes
|
||||
processing it. The workflow does not select the build for an App Store version
|
||||
or submit it for App Review.
|
||||
@@ -0,0 +1,95 @@
|
||||
# 移动端商店交付
|
||||
|
||||
GitHub Release 与移动端商店交付是两个独立操作:
|
||||
|
||||
- `bun run release` 只创建并审计 GitHub Release,不会访问 Google Play 或
|
||||
App Store Connect。
|
||||
- `bun run publish:stores` 针对一个已经存在的正式 Release tag,触发手动商店
|
||||
交付工作流。
|
||||
- 商店正式发布仍然是独立决策。Google Play Production 上传会保持 Draft,
|
||||
iOS 上传只到 App Store Connect/TestFlight。
|
||||
|
||||
## 安全模型
|
||||
|
||||
工作流检出不可变的 Release tag,而不是 `main`。开始任何商店构建前都会验证:
|
||||
|
||||
- tag 属于正式、非 Prerelease 的 GitHub Release;
|
||||
- Release 目标提交与 Git tag 指向同一个提交;
|
||||
- 与上一个正式 Release 相比,审计范围内确实包含移动端运行时代码变化;
|
||||
- 根版本和移动端 App 版本都与 Release tag 一致;
|
||||
- Android `versionCode` 已递增;
|
||||
- Google Play Production 请求再次输入了完全一致的 Release tag。
|
||||
|
||||
如果某个 Release 复用了上一版移动端二进制,工作流会主动拒绝。它不代表新的
|
||||
商店二进制,不应重复上传。
|
||||
|
||||
## 前置配置
|
||||
|
||||
在 GitHub 仓库中配置以下 Secrets:
|
||||
|
||||
- `EXPO_TOKEN`
|
||||
- `ANDROID_KEYSTORE_BASE64`
|
||||
- `ANDROID_KEYSTORE_PASSWORD`
|
||||
- `ANDROID_KEY_ALIAS`
|
||||
- `ANDROID_KEY_PASSWORD`
|
||||
|
||||
将 Google Play 服务账号密钥上传到 Android 应用的 EAS Submit Credentials。
|
||||
在 EAS 中配置 iOS 分发凭据和 App Store Connect API Key。凭据和私钥禁止提交
|
||||
到仓库。
|
||||
|
||||
创建以下 GitHub Environments:
|
||||
|
||||
- `store-delivery`:用于 Internal/Alpha/Beta 和 TestFlight 交付。
|
||||
- `store-production`:用于 Google Play Production 交付,并为该 Environment
|
||||
配置 Required Reviewers。
|
||||
|
||||
EAS Submit 要求应用已经在对应商店中创建;Google Play API 提交还要求服务账号
|
||||
拥有该应用的访问权限。配置方法参考官方
|
||||
[EAS Android 提交指南](https://docs.expo.dev/submit/android/)和
|
||||
[EAS Submit 配置参考](https://docs.expo.dev/submit/eas-json/)。
|
||||
|
||||
## 命令
|
||||
|
||||
同时交付到 Google Play Internal 与 App Store Connect/TestFlight:
|
||||
|
||||
```sh
|
||||
bun run publish:stores -- --release v1.7.0
|
||||
```
|
||||
|
||||
只将 Android 交付到封闭测试轨道:
|
||||
|
||||
```sh
|
||||
bun run publish:stores -- \
|
||||
--release v1.7.0 \
|
||||
--platform android \
|
||||
--android-track beta
|
||||
```
|
||||
|
||||
准备 Google Play Production Draft:
|
||||
|
||||
```sh
|
||||
bun run publish:stores -- \
|
||||
--release v1.7.0 \
|
||||
--platform android \
|
||||
--android-track production \
|
||||
--confirm-production v1.7.0
|
||||
```
|
||||
|
||||
使用 `--dry-run` 可以只输出将要触发的 GitHub 工作流,不实际启动。
|
||||
|
||||
## 各平台行为
|
||||
|
||||
### Google Play
|
||||
|
||||
自托管发布 Runner 会从指定 tag 构建签名 AAB,验证签名和 R8 Mapping,将两者
|
||||
保留为 GitHub Actions Artifacts,然后通过 EAS Submit 上传 AAB。
|
||||
|
||||
Internal、Alpha 和 Beta 配置会在对应测试轨道创建 Completed Release。
|
||||
Production 使用 `releaseStatus: draft` 和 `changesNotSentForReview: true`;
|
||||
完成审核和发布仍需在 Google Play Console 中手动操作。
|
||||
|
||||
### App Store Connect
|
||||
|
||||
EAS Build 从指定 tag 创建签名 iOS Archive,并递增远端 iOS Build Number。
|
||||
EAS Submit 将同一个构建上传到 App Store Connect;Apple 处理完成后,它会出现在
|
||||
TestFlight 中。工作流不会为 App Store 版本选择构建,也不会自动提交 App Review。
|
||||
+2
-1
@@ -34,8 +34,9 @@
|
||||
"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 scripts/release.test.mjs",
|
||||
"test:release-planning": "bun test scripts/plan-native-release.test.mjs scripts/check-native-release-assets.test.mjs scripts/release.test.mjs scripts/validate-store-delivery.test.mjs scripts/store-delivery.test.mjs",
|
||||
"release": "bun scripts/release.mjs",
|
||||
"publish:stores": "bun scripts/store-delivery.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/installation-location.test.mjs apps/desktop/src/main/resource-url.test.mjs apps/desktop/src/main/staged-resource.test.mjs apps/desktop/src/main/user-data-directory.test.mjs apps/desktop/src/main/window-open-policy.test.mjs apps/desktop/src/main/window-visibility.test.mjs apps/web/src/lib/api.test.mjs apps/web/src/lib/app-page-path.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/runtime.test.mjs apps/web/src/lib/sync-queue.test.mjs && bun run test:desktop:sidecar",
|
||||
"verify:desktop-package": "bun scripts/verify-desktop-package.mjs",
|
||||
|
||||
@@ -37,6 +37,8 @@ export const RELEASE_VALIDATIONS = [
|
||||
"scripts/plan-native-release.test.mjs",
|
||||
"scripts/check-native-release-assets.test.mjs",
|
||||
"scripts/release.test.mjs",
|
||||
"scripts/validate-store-delivery.test.mjs",
|
||||
"scripts/store-delivery.test.mjs",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const DEFAULT_REPOSITORY = "tianma-if/edgeever";
|
||||
const PLATFORMS = new Set(["android", "ios", "both"]);
|
||||
const ANDROID_TRACKS = new Set(["internal", "alpha", "beta", "production"]);
|
||||
|
||||
const usage = `Usage:
|
||||
bun run publish:stores -- --release vX.Y.Z [options]
|
||||
|
||||
Options:
|
||||
--release <tag> Required formal GitHub Release tag
|
||||
--platform <target> android, ios, or both (default: both)
|
||||
--android-track <track> internal, alpha, beta, or production (default: internal)
|
||||
--confirm-production <tag> Required when the Android track is production
|
||||
--repository <owner/name> GitHub repository (default: ${DEFAULT_REPOSITORY})
|
||||
--dry-run Print the workflow dispatch plan
|
||||
--help Show this help
|
||||
`;
|
||||
|
||||
export const parseStoreDeliveryArgs = (argv) => {
|
||||
const options = {
|
||||
releaseTag: "",
|
||||
platform: "both",
|
||||
androidTrack: "internal",
|
||||
productionConfirmation: "",
|
||||
repository: DEFAULT_REPOSITORY,
|
||||
dryRun: false,
|
||||
help: false,
|
||||
};
|
||||
const valueOptions = new Map([
|
||||
["--release", "releaseTag"],
|
||||
["--platform", "platform"],
|
||||
["--android-track", "androidTrack"],
|
||||
["--confirm-production", "productionConfirmation"],
|
||||
["--repository", "repository"],
|
||||
]);
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const argument = argv[index];
|
||||
if (argument === "--dry-run") {
|
||||
options.dryRun = true;
|
||||
continue;
|
||||
}
|
||||
if (argument === "--help") {
|
||||
options.help = true;
|
||||
continue;
|
||||
}
|
||||
const key = valueOptions.get(argument);
|
||||
if (!key) {
|
||||
throw new Error(`Unknown option: ${argument}`);
|
||||
}
|
||||
const value = argv[index + 1]?.trim();
|
||||
if (!value || value.startsWith("--")) {
|
||||
throw new Error(`${argument} requires a value.`);
|
||||
}
|
||||
options[key] = value;
|
||||
index += 1;
|
||||
}
|
||||
|
||||
if (options.help) {
|
||||
return options;
|
||||
}
|
||||
if (!/^v\d+\.\d+\.\d+$/.test(options.releaseTag)) {
|
||||
throw new Error("--release must use stable vX.Y.Z format.");
|
||||
}
|
||||
if (!PLATFORMS.has(options.platform)) {
|
||||
throw new Error("--platform must be android, ios, or both.");
|
||||
}
|
||||
if (!ANDROID_TRACKS.has(options.androidTrack)) {
|
||||
throw new Error(
|
||||
"--android-track must be internal, alpha, beta, or production.",
|
||||
);
|
||||
}
|
||||
if (
|
||||
options.androidTrack === "production" &&
|
||||
options.platform !== "ios" &&
|
||||
options.productionConfirmation !== options.releaseTag
|
||||
) {
|
||||
throw new Error(
|
||||
`Production delivery requires --confirm-production ${options.releaseTag}.`,
|
||||
);
|
||||
}
|
||||
if (!/^[^/\s]+\/[^/\s]+$/.test(options.repository)) {
|
||||
throw new Error("--repository must use owner/name format.");
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
const run = (options) => {
|
||||
const args = [
|
||||
"workflow",
|
||||
"run",
|
||||
"store-delivery.yml",
|
||||
"--repo",
|
||||
options.repository,
|
||||
"--ref",
|
||||
"main",
|
||||
"-f",
|
||||
`release_tag=${options.releaseTag}`,
|
||||
"-f",
|
||||
`platform=${options.platform}`,
|
||||
"-f",
|
||||
`android_track=${options.androidTrack}`,
|
||||
"-f",
|
||||
`production_confirmation=${options.productionConfirmation}`,
|
||||
];
|
||||
if (options.dryRun) {
|
||||
console.log(`gh ${args.join(" ")}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = spawnSync("gh", args, {
|
||||
cwd: resolve("."),
|
||||
encoding: "utf8",
|
||||
env: process.env,
|
||||
stdio: ["ignore", "pipe", "inherit"],
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`GitHub workflow dispatch failed with status ${result.status ?? 1}.`);
|
||||
}
|
||||
const output = String(result.stdout ?? "").trim();
|
||||
console.log(output || "Store delivery workflow dispatched.");
|
||||
};
|
||||
|
||||
if (
|
||||
process.argv[1] &&
|
||||
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
||||
) {
|
||||
try {
|
||||
const options = parseStoreDeliveryArgs(process.argv.slice(2));
|
||||
if (options.help) {
|
||||
console.log(usage);
|
||||
} else {
|
||||
run(options);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[store-delivery] failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { parseStoreDeliveryArgs } from "./store-delivery.mjs";
|
||||
|
||||
describe("store delivery command", () => {
|
||||
test("uses safe testing defaults", () => {
|
||||
expect(
|
||||
parseStoreDeliveryArgs(["--release", "v1.7.0"]),
|
||||
).toMatchObject({
|
||||
releaseTag: "v1.7.0",
|
||||
platform: "both",
|
||||
androidTrack: "internal",
|
||||
productionConfirmation: "",
|
||||
});
|
||||
});
|
||||
|
||||
test("accepts an explicit single platform", () => {
|
||||
expect(
|
||||
parseStoreDeliveryArgs([
|
||||
"--release",
|
||||
"v1.7.0",
|
||||
"--platform",
|
||||
"ios",
|
||||
]).platform,
|
||||
).toBe("ios");
|
||||
});
|
||||
|
||||
test("rejects malformed release tags", () => {
|
||||
expect(() =>
|
||||
parseStoreDeliveryArgs(["--release", "latest"])
|
||||
).toThrow("stable vX.Y.Z");
|
||||
});
|
||||
|
||||
test("requires exact confirmation for Google Play production", () => {
|
||||
expect(() =>
|
||||
parseStoreDeliveryArgs([
|
||||
"--release",
|
||||
"v1.7.0",
|
||||
"--platform",
|
||||
"android",
|
||||
"--android-track",
|
||||
"production",
|
||||
])
|
||||
).toThrow("--confirm-production v1.7.0");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { planNativeRelease } from "./plan-native-release.mjs";
|
||||
|
||||
const STABLE_RELEASE_TAG = /^v(\d+\.\d+\.\d+)$/;
|
||||
const PLATFORMS = new Set(["android", "ios", "both"]);
|
||||
const ANDROID_TRACKS = new Set(["internal", "alpha", "beta", "production"]);
|
||||
|
||||
export const validateStoreDelivery = ({
|
||||
releaseTag,
|
||||
rootVersion,
|
||||
mobileVersion,
|
||||
currentVersionCode,
|
||||
previousVersionCode,
|
||||
changedFiles,
|
||||
platform,
|
||||
androidTrack,
|
||||
productionConfirmation,
|
||||
}) => {
|
||||
const version = STABLE_RELEASE_TAG.exec(releaseTag)?.[1];
|
||||
if (!version) {
|
||||
throw new Error(`Release tag must use stable vX.Y.Z format: ${releaseTag}`);
|
||||
}
|
||||
if (!ANDROID_TRACKS.has(androidTrack)) {
|
||||
throw new Error(`Unsupported Google Play track: ${androidTrack}`);
|
||||
}
|
||||
if (!PLATFORMS.has(platform)) {
|
||||
throw new Error(`Unsupported store platform: ${platform}`);
|
||||
}
|
||||
|
||||
const mobilePlan = planNativeRelease("mobile", changedFiles);
|
||||
if (!mobilePlan.rebuild) {
|
||||
throw new Error(
|
||||
`${releaseTag} contains no mobile runtime changes; keep the existing store binary.`,
|
||||
);
|
||||
}
|
||||
if (rootVersion !== version || mobileVersion !== version) {
|
||||
throw new Error(
|
||||
`${releaseTag} requires package.json and apps/mobile/app.json versions to both equal ${version}.`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
!Number.isInteger(currentVersionCode) ||
|
||||
!Number.isInteger(previousVersionCode) ||
|
||||
currentVersionCode <= previousVersionCode
|
||||
) {
|
||||
throw new Error(
|
||||
`Android versionCode must increase beyond ${previousVersionCode}; received ${currentVersionCode}.`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
platform !== "ios" &&
|
||||
androidTrack === "production" &&
|
||||
productionConfirmation !== releaseTag
|
||||
) {
|
||||
throw new Error(
|
||||
`Production delivery requires --confirm-production ${releaseTag}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
versionCode: currentVersionCode,
|
||||
relevantChanges: mobilePlan.relevantChanges,
|
||||
};
|
||||
};
|
||||
|
||||
const readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
|
||||
|
||||
const run = () => {
|
||||
const [
|
||||
releaseTag,
|
||||
previousTag,
|
||||
platform = "both",
|
||||
androidTrack = "internal",
|
||||
productionConfirmation = "",
|
||||
] = process.argv.slice(2);
|
||||
if (!releaseTag || !previousTag) {
|
||||
console.error(
|
||||
"Usage: node scripts/validate-store-delivery.mjs <release-tag> <previous-tag> [platform] [android-track] [production-confirmation]",
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const git = (...args) =>
|
||||
execFileSync("git", args, { encoding: "utf8" }).trim();
|
||||
const changedFiles = git(
|
||||
"diff",
|
||||
"--name-only",
|
||||
`${previousTag}...${releaseTag}`,
|
||||
).split("\n").filter(Boolean);
|
||||
const rootPackage = readJson("package.json");
|
||||
const mobileConfig = readJson("apps/mobile/app.json");
|
||||
const previousMobileConfig = JSON.parse(
|
||||
git("show", `${previousTag}:apps/mobile/app.json`),
|
||||
);
|
||||
const result = validateStoreDelivery({
|
||||
releaseTag,
|
||||
rootVersion: rootPackage.version,
|
||||
mobileVersion: mobileConfig.expo.version,
|
||||
currentVersionCode: mobileConfig.expo.android.versionCode,
|
||||
previousVersionCode: previousMobileConfig.expo.android.versionCode,
|
||||
changedFiles,
|
||||
platform,
|
||||
androidTrack,
|
||||
productionConfirmation,
|
||||
});
|
||||
|
||||
process.stdout.write(`version=${result.version}\n`);
|
||||
process.stdout.write(`version_code=${result.versionCode}\n`);
|
||||
process.stdout.write("mobile_changed=true\n");
|
||||
process.stderr.write(
|
||||
`Store delivery ${releaseTag}: ${result.relevantChanges.join(", ")}\n`,
|
||||
);
|
||||
};
|
||||
|
||||
if (
|
||||
process.argv[1] &&
|
||||
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
||||
) {
|
||||
try {
|
||||
run();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[store-delivery] failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { validateStoreDelivery } from "./validate-store-delivery.mjs";
|
||||
|
||||
const validInput = {
|
||||
releaseTag: "v1.7.0",
|
||||
rootVersion: "1.7.0",
|
||||
mobileVersion: "1.7.0",
|
||||
currentVersionCode: 57,
|
||||
previousVersionCode: 56,
|
||||
changedFiles: ["apps/mobile/src/screens/WorkspaceScreen.tsx"],
|
||||
platform: "both",
|
||||
androidTrack: "internal",
|
||||
productionConfirmation: "",
|
||||
};
|
||||
|
||||
describe("store delivery validation", () => {
|
||||
test("accepts a mobile release with increasing versions", () => {
|
||||
expect(validateStoreDelivery(validInput)).toEqual({
|
||||
version: "1.7.0",
|
||||
versionCode: 57,
|
||||
relevantChanges: ["apps/mobile/src/screens/WorkspaceScreen.tsx"],
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects releases that reuse the existing mobile binary", () => {
|
||||
expect(() =>
|
||||
validateStoreDelivery({
|
||||
...validInput,
|
||||
changedFiles: ["apps/web/src/app/App.tsx"],
|
||||
})
|
||||
).toThrow("contains no mobile runtime changes");
|
||||
});
|
||||
|
||||
test("requires release and mobile versions to match", () => {
|
||||
expect(() =>
|
||||
validateStoreDelivery({ ...validInput, mobileVersion: "1.6.99" })
|
||||
).toThrow("versions to both equal 1.7.0");
|
||||
});
|
||||
|
||||
test("requires Android versionCode to increase", () => {
|
||||
expect(() =>
|
||||
validateStoreDelivery({ ...validInput, currentVersionCode: 56 })
|
||||
).toThrow("versionCode must increase");
|
||||
});
|
||||
|
||||
test("requires an exact confirmation for production delivery", () => {
|
||||
expect(() =>
|
||||
validateStoreDelivery({
|
||||
...validInput,
|
||||
androidTrack: "production",
|
||||
})
|
||||
).toThrow("--confirm-production v1.7.0");
|
||||
expect(
|
||||
validateStoreDelivery({
|
||||
...validInput,
|
||||
androidTrack: "production",
|
||||
productionConfirmation: "v1.7.0",
|
||||
}).version,
|
||||
).toBe("1.7.0");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user