diff --git a/.github/workflows/release-tamagotchi-steam.yml b/.github/workflows/release-tamagotchi-steam.yml new file mode 100644 index 000000000..4bc439438 --- /dev/null +++ b/.github/workflows/release-tamagotchi-steam.yml @@ -0,0 +1,281 @@ +name: Release Tamagotchi to Steam + +permissions: + contents: read + actions: read + +on: + release: + types: + - prereleased + workflow_dispatch: + inputs: + upload_to_steam: + description: Upload the completed build to Steam + required: true + default: false + type: boolean + release_branch: + description: Steam branch to set live + required: true + default: internal-test + type: string + build_description: + description: Optional Steam build description + required: false + type: string + +concurrency: + group: steam-deploy-tamagotchi-${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || 'internal-test' }} + cancel-in-progress: false + +jobs: + build: + name: Build ${{ matrix.depot }} depot + runs-on: ${{ matrix.os }} + env: + VITE_DISTRIBUTION: steam + VITE_DISABLE_CUSTOM_PROVIDERS: 'true' + VITE_DISABLE_FLUX_PURCHASE: 'true' + VITE_ENABLE_POSTHOG: 'false' + strategy: + fail-fast: false + matrix: + include: + - depot: windows + os: windows-latest + builder-args: --windows --x64 + arch: x64 + + - depot: macos + os: macos-26 + builder-args: --macos --arm64 + arch: arm64 + + - depot: linux + os: ubuntu-latest + builder-args: --linux --x64 + arch: x64 + + steps: + # The Linux desktop build needs more space than the default hosted runner provides. + - name: Free Linux disk space + if: matrix.depot == 'linux' + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 + + - uses: actions/checkout@v6 + + - name: Select Xcode 26.2 + if: matrix.depot == 'macos' + run: | + sudo xcode-select -s /Applications/Xcode_26.2.app + xcodebuild -version + + - name: Show macOS toolchain + if: matrix.depot == 'macos' + run: | + xcodebuild -version + actool --version + + - uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Install macOS system dependencies for node-canvas + if: matrix.depot == 'macos' + run: brew install pkg-config cairo pango jpeg giflib librsvg + + - uses: actions/setup-node@v6 + with: + node-version: lts/* + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - run: pnpm run build:packages + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Setup Godot + uses: chickensoft-games/setup-godot@v2 + with: + version: '4.7.1' + use-dotnet: true + include-templates: true + + - name: Export Godot Stage for Windows + if: matrix.depot == 'windows' + working-directory: ./engines/stage-tamagotchi-godot + shell: bash + run: | + mkdir -p out/win + godot --headless --export-release "Windows Desktop" out/win/godot-stage.exe + + - name: Export Godot Stage for macOS + if: matrix.depot == 'macos' + working-directory: ./engines/stage-tamagotchi-godot + run: | + mkdir -p out/mac + godot --headless --export-release "macOS" out/mac/godot-stage.app + + - name: Clean Godot Stage for Steam on macOS + if: matrix.depot == 'macos' + working-directory: ./engines/stage-tamagotchi-godot + run: | + # NOTICE: + # SteamPipe excludes *.pdb files after Electron signs the parent app. + # Removing Godot debug symbols and AppleDouble files before signing keeps macOS sealed resources aligned with the installed bundle. + # Source/context: Steam internal-test BuildID 24160350 and the game-ci SteamPipe FileExclusion "*.pdb" behavior. + # Removal condition: SteamPipe preserves these resources or the embedded Godot bundle no longer contains them. + find out/mac/godot-stage.app -name '*.pdb' -delete + find out/mac/godot-stage.app -name '._*' -delete + + - name: Export Godot Stage for Linux + if: matrix.depot == 'linux' + working-directory: ./engines/stage-tamagotchi-godot + run: | + mkdir -p out/linux + godot --headless --export-release "Linux ${{ matrix.arch }}" out/linux/godot-stage + + - name: Build Windows app + if: matrix.depot == 'windows' + run: pnpm run -F @proj-airi/stage-tamagotchi build && pnpm -F @proj-airi/stage-tamagotchi exec electron-builder build ${{ matrix.builder-args }} --publish=never + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Build macOS app + if: matrix.depot == 'macos' + run: | + echo "$CSC_CONTENT" | base64 --decode > apps/stage-tamagotchi/apple-developer-code-signing.p12 + export CSC_LINK="./apple-developer-code-signing.p12" + pnpm run -F @proj-airi/stage-tamagotchi build + pnpm -F @proj-airi/stage-tamagotchi exec electron-builder build ${{ matrix.builder-args }} --publish=never + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CSC_CONTENT: ${{ secrets.CSC_CONTENT }} + CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_DEVELOPER_APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_DEVELOPER_APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_DEVELOPER_TEAM_ID }} + + - name: Build Linux app + if: matrix.depot == 'linux' + run: pnpm run -F @proj-airi/stage-tamagotchi build && pnpm -F @proj-airi/stage-tamagotchi exec electron-builder build ${{ matrix.builder-args }} --publish=never + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Package Windows depot + if: matrix.depot == 'windows' + shell: pwsh + run: | + New-Item -ItemType Directory -Force steam-content/windows | Out-Null + Copy-Item apps/stage-tamagotchi/dist/win-unpacked/* steam-content/windows -Recurse -Force + tar -cf steam-content-windows.tar -C steam-content windows + + - name: Package macOS depot + if: matrix.depot == 'macos' + run: | + app_path="apps/stage-tamagotchi/dist/mac-arm64/AIRI.app" + if [[ ! -d "$app_path" ]]; then + echo "::error::No macOS app bundle was produced." + exit 1 + fi + + mkdir -p steam-content/macos + ditto --norsrc "$app_path" steam-content/macos/AIRI.app + find steam-content/macos -name '._*' -delete + COPYFILE_DISABLE=1 tar -cf steam-content-macos.tar -C steam-content macos + + - name: Package Linux depot + if: matrix.depot == 'linux' + run: | + mkdir -p steam-content/linux + cp -a apps/stage-tamagotchi/dist/linux-unpacked/. steam-content/linux/ + tar -cf steam-content-linux.tar -C steam-content linux + + - name: Upload depot artifact + uses: actions/upload-artifact@v7 + with: + name: steam-content-${{ matrix.depot }} + path: steam-content-${{ matrix.depot }}.tar + if-no-files-found: error + overwrite: true + + deploy: + name: Upload to Steam + if: ${{ github.event_name == 'release' || inputs.upload_to_steam }} + needs: build + runs-on: ubuntu-latest + env: + STEAM_APP_ID: '3885340' + STEAM_DEBUG_BRANCH: 'false' + STEAM_RELEASE_BRANCH: ${{ github.event_name == 'workflow_dispatch' && inputs.release_branch || 'internal-test' }} + STEAM_WINDOWS_DEPOT_ID: '3885342' + steps: + - name: Validate Steam branch + env: + RELEASE_BRANCH: ${{ env.STEAM_RELEASE_BRANCH }} + run: | + if [[ ! "$RELEASE_BRANCH" =~ ^[A-Za-z0-9._-]+$ ]]; then + echo "::error::Steam release branch may contain only letters, numbers, dots, underscores, and hyphens." + exit 1 + fi + + if [[ "$RELEASE_BRANCH" == "default" ]]; then + echo '::error::Steam release branch "default" must be promoted manually.' + exit 1 + fi + + - name: Normalize Steam build description + id: metadata + uses: actions/github-script@v9 + env: + INPUT_BUILD_DESCRIPTION: ${{ inputs.build_description }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + with: + result-encoding: string + script: | + const source = process.env.RELEASE_TAG || process.env.GITHUB_SHA + const fallback = `AIRI ${source} Steam candidate` + const normalizedDescription = (process.env.INPUT_BUILD_DESCRIPTION || fallback) + .replace(/[\r\n\t]+/g, ' ') + .replace(/[\u0000-\u001f\u007f]+/g, ' ') + .replaceAll('\\', '/') + .replaceAll('"', "'") + .replace(/ {2,}/g, ' ') + .trim() + .slice(0, 512) + + return normalizedDescription || fallback + + - name: Download Steam depots + uses: actions/download-artifact@v5 + with: + pattern: steam-content-* + path: steam-artifacts + merge-multiple: true + + - name: Prepare Steam upload root + run: | + mkdir -p steam-build + tar -xf steam-artifacts/steam-content-windows.tar -C steam-build + tar -xf steam-artifacts/steam-content-macos.tar -C steam-build + tar -xf steam-artifacts/steam-content-linux.tar -C steam-build + + - name: Deploy desktop builds to Steam + uses: game-ci/steam-deploy@v3.2.0 + with: + username: ${{ secrets.STEAM_USERNAME }} + configVdf: ${{ secrets.STEAM_CONFIG_VDF }} + appId: ${{ env.STEAM_APP_ID }} + firstDepotIdOverride: ${{ env.STEAM_WINDOWS_DEPOT_ID }} + buildDescription: ${{ steps.metadata.outputs.result }} + rootPath: steam-build + depot1Path: windows + depot2Path: macos + depot3Path: linux + releaseBranch: ${{ env.STEAM_RELEASE_BRANCH }} + debugBranch: ${{ env.STEAM_DEBUG_BRANCH }} diff --git a/apps/stage-tamagotchi/src/main/index.ts b/apps/stage-tamagotchi/src/main/index.ts index 4f6a148eb..e1bacb7cb 100644 --- a/apps/stage-tamagotchi/src/main/index.ts +++ b/apps/stage-tamagotchi/src/main/index.ts @@ -136,6 +136,7 @@ app.whenReady().then(async () => { const autoUpdater = injeca.provide('services:auto-updater', { dependsOn: { appConfig }, build: ({ dependsOn }) => setupAutoUpdater({ + enabled: import.meta.env.VITE_DISTRIBUTION !== 'steam', getStoredUpdateLane: () => dependsOn.appConfig.get()?.updateChannel, setStoredUpdateLane: (lane) => { const currentConfig = dependsOn.appConfig.get() diff --git a/apps/stage-tamagotchi/src/main/services/electron/auto-updater.test.ts b/apps/stage-tamagotchi/src/main/services/electron/auto-updater.test.ts index 48254d965..03e14815f 100644 --- a/apps/stage-tamagotchi/src/main/services/electron/auto-updater.test.ts +++ b/apps/stage-tamagotchi/src/main/services/electron/auto-updater.test.ts @@ -177,6 +177,24 @@ describe('setupAutoUpdater', () => { expect(fetchSpy).not.toHaveBeenCalled() }) + // https://github.com/moeru-ai/airi/pull/1827 + it('keeps storefront-managed distributions outside the GitHub updater flow (PR #1827)', async () => { + const fetchSpy = mockGitHubReleasesFetch() + const { setupAutoUpdater } = await import('./auto-updater') + const service = setupAutoUpdater({ enabled: false }) + + await service.checkForUpdates() + await service.downloadUpdate() + await service.quitAndInstall() + + expect(service.state.status).toBe('disabled') + expect(fetchSpy).not.toHaveBeenCalled() + expect(updaterState.instance.on).not.toHaveBeenCalled() + expect(updaterState.instance.checkForUpdates).not.toHaveBeenCalled() + expect(updaterState.instance.downloadUpdate).not.toHaveBeenCalled() + expect(updaterState.instance.quitAndInstall).not.toHaveBeenCalled() + }) + it('supports explicit stable lane selection for future dynamic channel switching', async () => { process.env.AIRI_UPDATE_CHANNEL = 'stable' mockGitHubReleasesFetch([ diff --git a/apps/stage-tamagotchi/src/main/services/electron/auto-updater.ts b/apps/stage-tamagotchi/src/main/services/electron/auto-updater.ts index cb21c32e1..513a6f298 100644 --- a/apps/stage-tamagotchi/src/main/services/electron/auto-updater.ts +++ b/apps/stage-tamagotchi/src/main/services/electron/auto-updater.ts @@ -260,7 +260,15 @@ export interface AutoUpdater { } export interface AutoUpdaterOptions { + /** + * Whether AIRI owns application updates for this distribution. + * + * @default true + */ + enabled?: boolean + /** Reads the release channel persisted by the application configuration. */ getStoredUpdateLane?: () => UpdateLane | undefined + /** Persists a release-channel change requested through updater IPC. */ setStoredUpdateLane?: (lane: UpdateLane | undefined) => void } @@ -268,7 +276,39 @@ function isPrereleaseVersion(version: string) { return (semver.prerelease(version)?.length ?? 0) > 0 } +/** + * Preserves the updater IPC contract when the storefront owns application updates. + * + * No method reaches Electron Updater or a release feed, while preference reads and + * subscriptions remain available to existing renderer consumers. + */ +function createDisabledAutoUpdater(options: AutoUpdaterOptions): AutoUpdater { + const state: AutoUpdaterState = { status: 'disabled' } + let storedPreferredLane = options.getStoredUpdateLane?.() + + return { + state, + async checkForUpdates() {}, + async downloadUpdate() {}, + async quitAndInstall() {}, + getPreferredUpdateLane() { + return storedPreferredLane + }, + async setPreferredUpdateLane(lane) { + storedPreferredLane = lane + options.setStoredUpdateLane?.(lane) + }, + subscribe(callback) { + callback(state) + return () => {} + }, + } +} + export function setupAutoUpdater(options: AutoUpdaterOptions = {}): AutoUpdater { + if (options.enabled === false) + return createDisabledAutoUpdater(options) + const semaphore = new Semaphore(1) const appVersion = app.getVersion() const isPrereleaseBuild = isPrereleaseVersion(appVersion) diff --git a/apps/stage-tamagotchi/src/renderer/pages/about.vue b/apps/stage-tamagotchi/src/renderer/pages/about.vue index 3e60d80ab..505ebb580 100644 --- a/apps/stage-tamagotchi/src/renderer/pages/about.vue +++ b/apps/stage-tamagotchi/src/renderer/pages/about.vue @@ -39,6 +39,7 @@ const { } = useAnalytics() const isDisabled = computed(() => updateState.value.status === 'disabled') +const isStoreManaged = import.meta.env.VITE_DISTRIBUTION === 'steam' const isLatestVersion = computed(() => { return updateState.value.status === 'not-available' && !isDisabled.value }) @@ -279,6 +280,7 @@ onMounted(() => { { : isLatestVersion ? t('tamagotchi.stage.about.update.actions.latest-version') : isDisabled - ? t('tamagotchi.stage.about.update.actions.disabled-dev') + ? t(isStoreManaged + ? 'tamagotchi.stage.about.update.actions.managed-by-store' + : 'tamagotchi.stage.about.update.actions.disabled-dev') : isError ? t('tamagotchi.stage.about.update.actions.retry-check') : t('tamagotchi.stage.about.update.actions.check-for-updates')" diff --git a/packages/i18n/src/locales/en/tamagotchi/stage.yaml b/packages/i18n/src/locales/en/tamagotchi/stage.yaml index f3cf31c41..7c5087e88 100644 --- a/packages/i18n/src/locales/en/tamagotchi/stage.yaml +++ b/packages/i18n/src/locales/en/tamagotchi/stage.yaml @@ -94,6 +94,7 @@ about: checking: Checking... latest-version: Latest version disabled-dev: Updates disabled in Dev + managed-by-store: Updates managed by your app store retry-check: Retry Check restart-silent: Restart to update silently restart-install: Restart to install update diff --git a/packages/i18n/src/locales/es/tamagotchi/stage.yaml b/packages/i18n/src/locales/es/tamagotchi/stage.yaml index c4a0fdb79..b787348ea 100644 --- a/packages/i18n/src/locales/es/tamagotchi/stage.yaml +++ b/packages/i18n/src/locales/es/tamagotchi/stage.yaml @@ -91,6 +91,7 @@ about: checking: Verificando... latest-version: Última versión disabled-dev: Actualizaciones desactivadas en Dev + managed-by-store: Las actualizaciones se gestionan desde la tienda de aplicaciones retry-check: Reintentar verificación restart-silent: Reiniciar para actualizar silenciosamente restart-install: Reiniciar e instalar actualización diff --git a/packages/i18n/src/locales/fr/tamagotchi/stage.yaml b/packages/i18n/src/locales/fr/tamagotchi/stage.yaml index 958269c93..935266083 100644 --- a/packages/i18n/src/locales/fr/tamagotchi/stage.yaml +++ b/packages/i18n/src/locales/fr/tamagotchi/stage.yaml @@ -91,6 +91,7 @@ about: checking: Vérification en cours... latest-version: Dernière version disabled-dev: Mises à jour désactivées en développement + managed-by-store: Les mises à jour sont gérées par la boutique d’applications retry-check: Réessayer la vérification restart-silent: Redémarrer pour mettre à jour silencieusement restart-install: Redémarrez pour installer la mise à jour diff --git a/packages/i18n/src/locales/ja/tamagotchi/stage.yaml b/packages/i18n/src/locales/ja/tamagotchi/stage.yaml index 8ab6efdac..3efe8293a 100644 --- a/packages/i18n/src/locales/ja/tamagotchi/stage.yaml +++ b/packages/i18n/src/locales/ja/tamagotchi/stage.yaml @@ -91,6 +91,7 @@ about: checking: 確認中… latest-version: 最新バージョン disabled-dev: Updates disabled in Dev + managed-by-store: アップデートはアプリストアによって管理されています retry-check: Retry Check restart-silent: Restart to update silently restart-install: 再起動してアップデートをインストールする diff --git a/packages/i18n/src/locales/ko/tamagotchi/stage.yaml b/packages/i18n/src/locales/ko/tamagotchi/stage.yaml index 61bb7cdf9..0e56041a5 100644 --- a/packages/i18n/src/locales/ko/tamagotchi/stage.yaml +++ b/packages/i18n/src/locales/ko/tamagotchi/stage.yaml @@ -91,6 +91,7 @@ about: checking: 확인 중... latest-version: 최신 버전 disabled-dev: Updates disabled in Dev + managed-by-store: 업데이트는 앱 스토어에서 관리됩니다 retry-check: 다시 확인하기 restart-silent: 재 시작 후 조용히 업데이트하기 restart-install: 재시작 후 업데이트하기 diff --git a/packages/i18n/src/locales/ru/tamagotchi/stage.yaml b/packages/i18n/src/locales/ru/tamagotchi/stage.yaml index cfc186516..11f8f3161 100644 --- a/packages/i18n/src/locales/ru/tamagotchi/stage.yaml +++ b/packages/i18n/src/locales/ru/tamagotchi/stage.yaml @@ -91,6 +91,7 @@ about: checking: Проверка... latest-version: Последняя версия disabled-dev: Обновления отключены в Dev + managed-by-store: Обновления управляются магазином приложений retry-check: Повторить проверку restart-silent: Перезапустите, чтобы обновить автоматически restart-install: Перезапустите для установки обновления diff --git a/packages/i18n/src/locales/vi/tamagotchi/stage.yaml b/packages/i18n/src/locales/vi/tamagotchi/stage.yaml index 504f2d44f..fd52c1a34 100644 --- a/packages/i18n/src/locales/vi/tamagotchi/stage.yaml +++ b/packages/i18n/src/locales/vi/tamagotchi/stage.yaml @@ -91,6 +91,7 @@ about: checking: Đang kiểm tra... latest-version: Phiên bản mới nhất disabled-dev: Các bản cập nhật bị vô hiệu trong Dev + managed-by-store: Bản cập nhật do cửa hàng ứng dụng quản lý retry-check: Retry Check restart-silent: Khởi động lại để cập nhật trong im lặng restart-install: Khởi động lại để tải bản cập nhật diff --git a/packages/i18n/src/locales/zh-Hans/tamagotchi/stage.yaml b/packages/i18n/src/locales/zh-Hans/tamagotchi/stage.yaml index 745b26f77..a8b3c357e 100644 --- a/packages/i18n/src/locales/zh-Hans/tamagotchi/stage.yaml +++ b/packages/i18n/src/locales/zh-Hans/tamagotchi/stage.yaml @@ -91,6 +91,7 @@ about: checking: 正在检查... latest-version: 最新版本 disabled-dev: 已禁用开发环境时的更新 + managed-by-store: 更新由应用商店管理 retry-check: 重新检查 restart-silent: 重新启动以静默更新 restart-install: 重启并安装更新 diff --git a/packages/i18n/src/locales/zh-Hant/tamagotchi/stage.yaml b/packages/i18n/src/locales/zh-Hant/tamagotchi/stage.yaml index c0dac0eaf..4eed3b4e7 100644 --- a/packages/i18n/src/locales/zh-Hant/tamagotchi/stage.yaml +++ b/packages/i18n/src/locales/zh-Hant/tamagotchi/stage.yaml @@ -91,6 +91,7 @@ about: checking: Checking... latest-version: Latest version disabled-dev: Updates disabled in Dev + managed-by-store: 更新由應用程式商店管理 retry-check: Retry Check restart-silent: Restart to update silently restart-install: Restart to install update diff --git a/vite-env.d.ts b/vite-env.d.ts index 27e806e4b..193a95051 100644 --- a/vite-env.d.ts +++ b/vite-env.d.ts @@ -1,6 +1,7 @@ /// interface ImportMetaEnv { + readonly VITE_DISTRIBUTION?: 'direct' | 'steam' readonly VITE_DISABLE_FLUX_PURCHASE?: string readonly VITE_DISABLE_CUSTOM_PROVIDERS?: string readonly VITE_ENABLE_POSTHOG?: string