import * as google from '@googleapis/androidpublisher' import * as fs from "fs" import * as http from 'https' import * as matrix from 'matrix-js-sdk' import * as url from 'url' const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); const auth = new google.auth.GoogleAuth({ keyFile: '.secrets/service-account.json', scopes: ['https://www.googleapis.com/auth/androidpublisher'], }) const androidPublisher = google.androidpublisher({ version: 'v3', auth: auth, }) const universalApkPath = `${__dirname}/universal.apk` export const release = async (github, version, applicationId, artifacts, config) => { const appEditId = await startPlayRelease(applicationId) console.log("Uploading bundle...") await uploadBundle(appEditId, applicationId, artifacts.bundle) console.log("Uploading mapping...") await uploadMappingFile(appEditId, version.code, applicationId, artifacts.mapping) console.log("Assign artifacts to beta release...") await addReleaseToTrack(appEditId, version, applicationId) console.log("Commiting draft release...") await androidPublisher.edits.commit({ editId: appEditId, packageName: applicationId, }).catch((error) => Promise.reject(error.response.data)) console.log("Downloading generated universal apk...") await dowloadSignedUniversalApk( version, applicationId, await auth.getAccessToken(), universalApkPath ) const releaseResult = await github.rest.repos.createRelease({ owner: config.owner, repo: config.repo, tag_name: version.name, prerelease: false, generate_release_notes: true, }) console.log(releaseResult.data.id) console.log("Uploading universal apk...") await github.rest.repos.uploadReleaseAsset({ owner: config.owner, repo: config.repo, release_id: releaseResult.data.id, name: `universal-${version.name}.apk`, data: fs.readFileSync(universalApkPath) }) console.log("Uploading foss apk...") await github.rest.repos.uploadReleaseAsset({ owner: config.owner, repo: config.repo, release_id: releaseResult.data.id, name: `foss-signed-${version.name}.apk`, data: fs.readFileSync(artifacts.fossApkPath) }) console.log("Promoting beta draft release to live...") await promoteDraftToLive(applicationId) console.log("Sending message to room...") await sendReleaseMessage(releaseResult.data, config) } const startPlayRelease = async (applicationId) => { const result = await androidPublisher.edits.insert({ packageName: applicationId }).catch((error) => Promise.reject(error.response.data)) return result.data.id } const uploadBundle = async (appEditId, applicationId, bundleReleaseFile) => { const res = await androidPublisher.edits.bundles.upload({ packageName: applicationId, editId: appEditId, media: { mimeType: 'application/octet-stream', body: fs.createReadStream(bundleReleaseFile) } }).catch((error) => Promise.reject(error.response.data)) return res.data } const uploadMappingFile = async (appEditId, versionCode, applicationId, mappingFilePath) => { await androidPublisher.edits.deobfuscationfiles.upload({ packageName: applicationId, editId: appEditId, apkVersionCode: versionCode, deobfuscationFileType: 'proguard', media: { mimeType: 'application/octet-stream', body: fs.createReadStream(mappingFilePath) } }).catch((error) => Promise.reject(error.response.data)) } const addReleaseToTrack = async (appEditId, version, applicationId) => { const result = await androidPublisher.edits.tracks .update({ editId: appEditId, packageName: applicationId, track: "beta", requestBody: { track: "beta", releases: [ { name: version.name, status: "draft", releaseNotes: { language: "en-GB", text: "Bug fixes and improvments - See https://github.com/ouchadam/small-talk/releases for more details", }, versionCodes: [version.code] } ] } }) .catch((error) => Promise.reject(error.response.data)) return result.data; } const dowloadSignedUniversalApk = async (version, applicationId, authToken, outputFile) => { console.log("fetching universal apk") const apkRes = await androidPublisher.generatedapks.list({ packageName: applicationId, versionCode: version.code, }) const apks = apkRes.data.generatedApks const id = apks[0].generatedUniversalApk.downloadId console.log(`downloading: ${id}`) const downloadUrl = `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${applicationId}/generatedApks/${version.code}/downloads/${id}:download?alt=media` const options = { headers: { "Authorization": `Bearer ${authToken}` } } await downloadToFile(downloadUrl, options, outputFile) } const downloadToFile = async (url, options, outputFile) => { return new Promise((resolve, error) => { http.get(url, options, (response) => { const file = fs.createWriteStream(outputFile) response.pipe(file) file.on("finish", () => { file.close() resolve() }) file.on("error", (cause) => { error(cause) }) }).on("error", (cause) => { error(cause) }) }) } const promoteDraftToLive = async (applicationId) => { const editId = await startPlayRelease(applicationId) await androidPublisher.edits.tracks .update({ editId: editId, packageName: applicationId, track: "beta", requestBody: { track: "beta", releases: [ { status: "completed", } ] } }) .catch((error) => Promise.reject(error.response.data)) await androidPublisher.edits.commit({ editId: editId, packageName: applicationId, }).catch((error) => Promise.reject(error.response.data)) } const sendReleaseMessage = async (release, config) => { const matrixAuth = JSON.parse(fs.readFileSync('.secrets/matrix.json')) const client = matrix.createClient(matrixAuth) const content = { "body": `New release`, "format": "org.matrix.custom.html", "formatted_body": `New release rolling out ${release.tag_name}`, "msgtype": "m.text" } await client.sendEvent(config.matrixRoomId, "m.room.message", content, "") }