From b68a9dd25c50603b1463614668248c24973a019c Mon Sep 17 00:00:00 2001 From: Jonathan Romano Date: Fri, 6 Sep 2024 16:21:08 -0400 Subject: [PATCH] Do actual build uploads --- .../workflows/google-play/package-lock.json | 151 ++++++++++++++++++ .github/workflows/google-play/package.json | 6 + .github/workflows/google-play/src/cli.js | 20 +++ .github/workflows/google-play/src/index.js | 13 ++ .../google-play/src/play-store-api.js | 91 +++++++++++ .github/workflows/upload-build.yml | 19 +++ 6 files changed, 300 insertions(+) create mode 100644 .github/workflows/google-play/package-lock.json create mode 100644 .github/workflows/google-play/package.json create mode 100644 .github/workflows/google-play/src/cli.js create mode 100644 .github/workflows/google-play/src/index.js create mode 100644 .github/workflows/google-play/src/play-store-api.js diff --git a/.github/workflows/google-play/package-lock.json b/.github/workflows/google-play/package-lock.json new file mode 100644 index 0000000..cff16ea --- /dev/null +++ b/.github/workflows/google-play/package-lock.json @@ -0,0 +1,151 @@ +{ + "name": "google-play", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "jsonwebtoken": "^9.0.2" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + } + } +} diff --git a/.github/workflows/google-play/package.json b/.github/workflows/google-play/package.json new file mode 100644 index 0000000..9e2aab7 --- /dev/null +++ b/.github/workflows/google-play/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "dependencies": { + "jsonwebtoken": "^9.0.2" + } +} diff --git a/.github/workflows/google-play/src/cli.js b/.github/workflows/google-play/src/cli.js new file mode 100644 index 0000000..a3c6f20 --- /dev/null +++ b/.github/workflows/google-play/src/cli.js @@ -0,0 +1,20 @@ +// @ts-check + +export function parseArgs() { + const args = process.argv.slice(2); + const mode = args.shift(); + if (!mode) throw new Error('Missing arguments'); + + if (mode === 'upload') { + if (args.length < 2) throw new Error('Missing arguments'); + const [apiKey, bundlePath] = args; + return { + /** @type {'upload'} */ + mode: 'upload', + apiKey: JSON.parse(Buffer.from(apiKey, 'base64').toString()), + bundlePath + }; + } else { + throw new Error('Invalid mode') + } +} \ No newline at end of file diff --git a/.github/workflows/google-play/src/index.js b/.github/workflows/google-play/src/index.js new file mode 100644 index 0000000..f134124 --- /dev/null +++ b/.github/workflows/google-play/src/index.js @@ -0,0 +1,13 @@ +// @ts-check +import { parseArgs } from "./cli.js"; +import { PlayStoreApi } from "./play-store-api.js"; + +const PACKAGE_NAME = 'org.eternagame.mob'; + +const args = parseArgs(); +const playStoreApi = await PlayStoreApi.create(args.apiKey); +if (args.mode === 'upload') { + const editId = await playStoreApi.insertEdit(PACKAGE_NAME); + await playStoreApi.uploadBundle(PACKAGE_NAME, editId, args.bundlePath); + await playStoreApi.commitEdit(PACKAGE_NAME, editId); +} diff --git a/.github/workflows/google-play/src/play-store-api.js b/.github/workflows/google-play/src/play-store-api.js new file mode 100644 index 0000000..f44d7fd --- /dev/null +++ b/.github/workflows/google-play/src/play-store-api.js @@ -0,0 +1,91 @@ +// @ts-check +import { readFile } from 'fs/promises'; +import jwt from 'jsonwebtoken'; + +export class PlayStoreApi { + /** + * @param {string} accessToken + * @access private + */ + constructor(accessToken) { + this.accessToken = accessToken; + } + + /** + * @param {{ + * type: string; + * project_id: string; + * private_key_id: string; + * private_key: string; + * client_email: string; + * client_id: string; + * auth_uri: string; + * token_uri: string; + * auth_provider_x509_cert_url: string; + * client_x509_cert_url: string; + * universe_domain: string; + * }} apiKey + */ + static async create(apiKey) { + const authToken = jwt.sign({ + scope: 'https://www.googleapis.com/auth/androidpublisher' + }, apiKey['private_key'], { + algorithm: 'RS256', + keyid: apiKey['private_key_id'], + issuer: apiKey['client_email'], + audience: apiKey['token_uri'], + expiresIn: '5m', + }); + const res = await fetch(apiKey['token_uri'], { + method: 'POST', + body: new URLSearchParams({ + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion: authToken + }).toString() + }); + const parsed = await res.json(); + return new PlayStoreApi(parsed['access_token']); + } + + /** + * @param {string} packageName + * @returns {Promise} edit ID + */ + async insertEdit(packageName) { + const res = await fetch(`https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${packageName}/edits`, { + method: 'POST', + headers: { + Authorization: `Bearer ${this.accessToken}` + } + }); + const parsed = await res.json(); + return parsed['id']; + } + + /** + * @param {string} packageName + * @param {string} editId + * @param {string} bundlePath + */ + async uploadBundle(packageName, editId, bundlePath) { + const bundle = await readFile(bundlePath); + await fetch(`https://androidpublisher.googleapis.com/upload/androidpublisher/v3/applications/${packageName}/edits/${editId}/bundles?uploadType=media`, { + method: 'POST', + headers: { + Authorization: `Bearer ${this.accessToken}`, + 'Content-Type': 'application/octet-stream', + 'Content-Length': bundle.length.toString(), + }, + body: bundle + }); + } + + async commitEdit(packageName, editId) { + await fetch(`https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${packageName}/edits/${editId}:commit`, { + method: 'POST', + headers: { + Authorization: `Bearer ${this.accessToken}`, + }, + }); + } +} \ No newline at end of file diff --git a/.github/workflows/upload-build.yml b/.github/workflows/upload-build.yml index 8cec2f9..c580d93 100644 --- a/.github/workflows/upload-build.yml +++ b/.github/workflows/upload-build.yml @@ -23,11 +23,24 @@ jobs: run-id: ${{ inputs.run-id }} - name: Decrypt build run: gpg --decrypt --output=./Eterna.ipa --passphrase ${{ secrets.ARTIFACT_ENCRYPTION_KEY }} --batch release-ios + - name: Set up App Store Connect API key + env: + APP_STORE_CONNECT_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }} + run: echo $APP_STORE_CONNECT_API_KEY_BASE64 | base64 --decode > "$RUNNER_TEMP/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8" + #- name: Upload to App Store + # run: API_PRIVATE_KEYS_DIR=$RUNNER_TEMP xcrun altool --upload-package Eterna.ipa --type ios --apiIssuer ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} --apiKey ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} upload-android: runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 + with: + ref: ci + submodules: false + persist-credentials: false + sparse-checkout: .github/**/* + sparse-checkout-cone-mode: false - uses: actions/download-artifact@v4 with: name: release-android @@ -35,3 +48,9 @@ jobs: run-id: ${{ inputs.run-id }} - name: Decrypt build run: gpg --decrypt --output=./app-release.aab --passphrase ${{ secrets.ARTIFACT_ENCRYPTION_KEY }} --batch release-android + - name: Install dependencies for upload script + run: npm ci + working-directory: ./.github/workflows/google-play + - run: ls .github/workflows/google-play + #- name: Upload to Google Play + # run: node ./.github/workflows/google-play \ No newline at end of file