diff --git a/.github/workflows/desktop-package.yml b/.github/workflows/desktop-package.yml index 50e7bdf341..bf9df5f4c0 100644 --- a/.github/workflows/desktop-package.yml +++ b/.github/workflows/desktop-package.yml @@ -757,19 +757,17 @@ jobs: run: | set -euo pipefail shopt -s globstar + # AppImage, deb, and rpm files are already copied into the + # canonical release-updater-assets directory above. Publishing the + # raw Tauri bundle names as well creates a second public copy of + # every Linux installer and doubles the signature count. node scripts/stage-github-release-assets.mjs \ --out-dir release-upload-assets \ release-updater-assets/* \ release-manual-assets/*.exe \ release-manual-assets/*.exe.sig \ - release-assets/**/*.AppImage \ - release-assets/**/*.AppImage.sig \ - release-assets/**/*.deb \ - release-assets/**/*.deb.sig \ release-assets/**/*.dmg \ release-assets/**/*.dmg.sig \ - release-assets/**/*.rpm \ - release-assets/**/*.rpm.sig \ release-assets/minisign.pub \ linux-release-assets/openbitfun-cli-*.tar.gz \ linux-release-assets/openbitfun-cli-*.tar.gz.sha256 \ @@ -827,9 +825,25 @@ jobs: esac gh release upload "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" \ --clobber release-upload-assets/* + MOBILE_VERSION="$(sed -nE 's/.*versionName = "([^"]+)".*/\1/p' \ + src/apps/mobile/android/app/build.gradle.kts 2>/dev/null | head -n1 || true)" + MOBILE_VERSION="${MOBILE_VERSION:-$RELEASE_VERSION}" + gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" \ + --json body --jq .body > release-notes.md + if ! grep -q '' release-notes.md; then + cat >> release-notes.md < + ### Android mobile + + Android mobile version **${MOBILE_VERSION}** is published separately as a prerelease: + [Download the Android prerelease](https://github.com/${GITHUB_REPOSITORY}/releases/tag/mobile-v${MOBILE_VERSION}). + The current APK uses the runner-generated debug signing key. + EOF + fi gh release edit "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" \ - --title "OpenBitFun ${RELEASE_VERSION}" --draft=false \ - --prerelease="${RELEASE_PRERELEASE}" --latest="${RELEASE_LATEST}" + --title "OpenBitFun ${RELEASE_VERSION}" --notes-file release-notes.md \ + --draft=false --prerelease="${RELEASE_PRERELEASE}" --latest="${RELEASE_LATEST}" - name: Verify published legacy update feeds if: needs.prepare.outputs.release_channel == 'stable' diff --git a/.github/workflows/mobile-package.yml b/.github/workflows/mobile-package.yml new file mode 100644 index 0000000000..57c6bd5dc4 --- /dev/null +++ b/.github/workflows/mobile-package.yml @@ -0,0 +1,133 @@ +name: Mobile Package + +on: + push: + tags: + - 'mobile-v*' + workflow_dispatch: + inputs: + tag_name: + description: 'Mobile release tag (for example mobile-v1.0.0).' + required: true + type: string + +permissions: + contents: write + +concurrency: + group: mobile-package-${{ github.event.release.tag_name || inputs.tag_name || github.ref_name }} + cancel-in-progress: false + +jobs: + prepare: + name: Prepare mobile release + runs-on: ubuntu-latest + outputs: + tag_name: ${{ steps.meta.outputs.tag_name }} + version: ${{ steps.meta.outputs.version }} + prerelease: ${{ steps.meta.outputs.prerelease }} + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ inputs.tag_name || github.ref_name }} + fetch-depth: 1 + + - name: Resolve mobile version + id: meta + shell: bash + env: + INPUT_TAG_NAME: ${{ inputs.tag_name }} + REF_NAME: ${{ github.ref_name }} + run: | + set -euo pipefail + TAG="${INPUT_TAG_NAME:-${REF_NAME}}" + case "$TAG" in + mobile-v*) ;; + *) echo "Expected a mobile-v* tag, got $TAG" >&2; exit 1 ;; + esac + VERSION="${TAG#mobile-v}" + node scripts/verify-release-version-sync.mjs --mobile-version "$VERSION" + if [[ "$VERSION" == *-* ]]; then + PRERELEASE=true + else + PRERELEASE=false + fi + printf 'tag_name=%s\nversion=%s\nprerelease=%s\n' \ + "$TAG" "$VERSION" "$PRERELEASE" >> "$GITHUB_OUTPUT" + + android: + name: Android package + needs: prepare + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ needs.prepare.outputs.tag_name }} + + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '21' + + - uses: gradle/actions/setup-gradle@v4 + + - name: Configure Android release signing + shell: bash + run: | + set -euo pipefail + echo "Using the runner-generated Android debug keystore for this mobile prerelease." + + - name: Build signed Android package + working-directory: src/apps/mobile/android + run: ./gradlew :app:assembleRelease --no-daemon + + - name: Stage Android release assets + shell: bash + env: + VERSION: ${{ needs.prepare.outputs.version }} + run: | + set -euo pipefail + mkdir -p mobile-release-assets + suffix='-debug' + cp src/apps/mobile/android/app/build/outputs/apk/release/app-release.apk \ + "mobile-release-assets/OpenBitFun-Mobile-${VERSION}-android${suffix}.apk" + (cd mobile-release-assets && sha256sum ./*.apk > SHA256SUMS) + + - uses: actions/upload-artifact@v6 + with: + name: openbitfun-mobile-${{ needs.prepare.outputs.tag_name }}-android + path: mobile-release-assets/* + if-no-files-found: error + retention-days: 7 + + publish: + name: Publish mobile release + needs: [prepare, android] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ needs.prepare.outputs.tag_name }} + + - uses: actions/download-artifact@v7 + with: + name: openbitfun-mobile-${{ needs.prepare.outputs.tag_name }}-android + path: mobile-release-assets + + - name: Create mobile release + env: + GH_TOKEN: ${{ github.token }} + TAG_NAME: ${{ needs.prepare.outputs.tag_name }} + VERSION: ${{ needs.prepare.outputs.version }} + PRERELEASE: true + shell: bash + run: | + set -euo pipefail + VERSION="$VERSION (debug signing)" + gh release create "$TAG_NAME" \ + --repo "$GITHUB_REPOSITORY" \ + --verify-tag \ + --title "OpenBitFun Mobile $VERSION" \ + --generate-notes \ + --prerelease="$PRERELEASE" \ + mobile-release-assets/* diff --git a/README.md b/README.md index 14cc39e567..aae4e69c63 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ OpenBitFun is a highly customizable, open-source agent workspace that keeps work **If this is the kind of agent workspace you want, ⭐ Star OpenBitFun and help more people discover it.** -[Download](https://openbitfun.com/download) · [Mini App Marketplace](https://market.openbitfun.com/miniapp/) · [Contribute](./CONTRIBUTING.md) +[Download Desktop](https://openbitfun.com/download) · [Mobile Releases](https://github.com/GCWing/OpenBitFun/releases?q=mobile-&expanded=true) · [Mini App Marketplace](https://market.openbitfun.com/miniapp/) · [Contribute](./CONTRIBUTING.md) [![Website](https://img.shields.io/badge/Website-openbitfun.com-0b7285?style=flat-square)](https://openbitfun.com/) [![Core code: MIT](https://img.shields.io/badge/core_code-MIT-yellow?style=flat-square)](./LICENSE) diff --git a/README.zh-CN.md b/README.zh-CN.md index dc4b438cc3..059a86891e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -12,7 +12,7 @@ OpenBitFun 是一个高度可定制的开源 Agent 工作台,让任务按你 **如果你也期待这样的 Agent,欢迎点亮 ⭐ Star,让更多人发现 OpenBitFun。** -[下载体验](https://openbitfun.com/zh/download) · [Mini App 市场](https://market.openbitfun.com/miniapp/) · [参与共建](./CONTRIBUTING_CN.md) +[下载桌面端](https://openbitfun.com/zh/download) · [移动端 Release](https://github.com/GCWing/OpenBitFun/releases?q=mobile-&expanded=true) · [Mini App 市场](https://market.openbitfun.com/miniapp/) · [参与共建](./CONTRIBUTING_CN.md) [![官网](https://img.shields.io/badge/%E5%AE%98%E7%BD%91-openbitfun.com-0b7285?style=flat-square)](https://openbitfun.com/zh) [![Core code: MIT](https://img.shields.io/badge/core_code-MIT-yellow?style=flat-square)](./LICENSE) diff --git a/docs/development/releasing.md b/docs/development/releasing.md index 122c26958c..be2d33378f 100644 --- a/docs/development/releasing.md +++ b/docs/development/releasing.md @@ -22,6 +22,22 @@ its tag automatically. Release creation and editing use GitHub CLI without `--target`: sending `target_commitish` for workflow-changing commits can require `workflows:write`, even for an existing tag. Asset upload precedes publication. +## Mobile Android releases + +Mobile Android packages are released independently from Desktop packages. Use a +`mobile-vMAJOR.MINOR.PATCH` tag (or a prerelease suffix such as +`mobile-v1.1.0-beta.1`) to trigger [`Mobile Package`](../../.github/workflows/mobile-package.yml). +The workflow builds an Android APK with the runner-generated debug keystore and +publishes it to its own prerelease GitHub Release. It does not add Android assets +to the Desktop `v*` Release. A production signing key can be introduced later +without changing the mobile tag or release layout. Mobile Android versions are +checked as a group across the Android, iOS, and HarmonyOS version files, but they +do not need to equal the Desktop version. + +GitHub exposes one repository-wide `Latest` release. Desktop release notes also +include the Android version and a link to the corresponding Mobile prerelease, +while the Desktop release itself contains no Android APK assets. + ## Legacy update isolation Desktop 1.x reads `latest-v1.json`; CLI 1.x reads `linux-binaries-v1.json`, diff --git a/scripts/check-github-config.test.mjs b/scripts/check-github-config.test.mjs index 280733ddcc..5435132d42 100644 --- a/scripts/check-github-config.test.mjs +++ b/scripts/check-github-config.test.mjs @@ -1094,10 +1094,46 @@ test('stages unique release asset names before publishing', () => { /release-assets\/\*\*\/\*\.sig(?:\s|\\)/, 'raw updater signatures have colliding names across macOS architectures', ); + assert.doesNotMatch( + steps[stagingIndex].run, + /release-assets\/\*\*\/\*(?:\.AppImage|\.deb|\.rpm)(?:\s|\\)/, + 'raw Linux bundles must not duplicate canonical updater assets in the public release', + ); } assert.match(steps[uploadIndex].run, /--clobber release-upload-assets\/\*/); }); +test('Desktop release notes link the separately published Android prerelease', () => { + const workflow = yaml.parse( + readFileSync( + path.join(repoRoot, '.github/workflows/desktop-package.yml'), + 'utf8', + ), + ); + const uploadStep = workflow.jobs['upload-release-assets'].steps.find( + (step) => step.name === 'Upload to release', + ); + assert.match(uploadStep.run, /mobile-v\$\{MOBILE_VERSION\}/); + assert.match(uploadStep.run, /runner-generated debug signing key/); + assert.match(uploadStep.run, /--notes-file release-notes\.md/); +}); + +test('Mobile Package publishes Android assets from mobile-v tags', () => { + const workflow = yaml.parse( + readFileSync(path.join(repoRoot, '.github/workflows/mobile-package.yml'), 'utf8'), + ); + assert.ok(workflow.on.push.tags.includes('mobile-v*')); + assert.match( + workflow.jobs.android.steps.find((step) => step.name === 'Build signed Android package').run, + /:app:assembleRelease/, + ); + assert.equal(workflow.jobs.publish.needs.includes('android'), true); + assert.match( + workflow.jobs.publish.steps.find((step) => step.name === 'Create mobile release').run, + /gh release create/, + ); +}); + test('Desktop packaging installs Bun before preparing the OpenCode extension Host', () => { const workflow = yaml.parse( readFileSync(path.join(repoRoot, '.github/workflows/desktop-package.yml'), 'utf8'),