import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const root = join(dirname(fileURLToPath(import.meta.url)), '..'); const gradlePath = join(root, 'android', 'app', 'build.gradle'); const aabPath = join(root, 'android', 'app', 'build', 'outputs', 'bundle', 'release', 'app-release.aab'); const gradle = readFileSync(gradlePath, 'utf8'); const versionCode = process.env.HESYCHIA_VERSION_CODE || gradle.match(/hesychiaVersionCodeFloor\s*=\s*(\d+)/)?.[1]; const versionName = process.env.HESYCHIA_VERSION_NAME || gradle.match(/hesychiaVersionNameFloor\s*=\s*"([^"]+)"/)?.[1]; if (!versionCode || !versionName) { console.error( `Could not read versionCode/versionName from the environment or ${gradlePath}`, ); process.exit(1); } const tag = `android-v${versionName}-${versionCode}`; const assetName = `hesychia-${versionName}-${versionCode}.aab`; const token = process.env.GITEA_TOKEN || process.env.GITHUB_TOKEN; const repository = process.env.GITHUB_REPOSITORY || process.env.GITEA_REPOSITORY; const sha = process.env.GITEA_SHA || process.env.GITHUB_SHA; const refName = process.env.GITEA_REF_NAME || process.env.GITHUB_REF_NAME || sha; const apiUrl = ( process.env.GITHUB_API_URL || process.env.GITEA_API_URL || (process.env.GITHUB_SERVER_URL ? `${process.env.GITHUB_SERVER_URL.replace(/\/$/, '')}/api/v1` : '') ).replace(/\/$/, ''); if (!token || !repository || !sha || !apiUrl) { console.error('Missing required env for Gitea Android release:', { apiUrl: Boolean(apiUrl), token: Boolean(token), repository, sha, }); process.exit(1); } const [owner, repo] = repository.split('/'); if (!owner || !repo) { console.error(`GITHUB_REPOSITORY must be owner/repo, got ${repository}`); process.exit(1); } async function api(path, { method = 'GET', json, form } = {}) { const headers = { Authorization: `token ${token}`, Accept: 'application/json', }; let body; if (json !== undefined) { headers['Content-Type'] = 'application/json'; body = JSON.stringify(json); } else if (form) { body = form; } const response = await fetch(`${apiUrl}/repos/${owner}/${repo}${path}`, { method, headers, body, }); const text = await response.text(); let payload = null; if (text) { try { payload = JSON.parse(text); } catch { payload = text; } } return { status: response.status, payload }; } function releaseUrl(release) { return release?.html_url || release?.url || tag; } function hasAsset(release) { return Array.isArray(release?.assets) && release.assets.some((asset) => asset.name === assetName); } async function uploadAsset(releaseId) { const bytes = readFileSync(aabPath); const form = new FormData(); form.append('attachment', new Blob([bytes]), assetName); const { status, payload } = await api( `/releases/${releaseId}/assets?name=${encodeURIComponent(assetName)}`, { method: 'POST', form }, ); if (status < 200 || status >= 300) { throw new Error(`Asset upload failed (${status}): ${JSON.stringify(payload)}`); } } const body = [ `Play store bundle for the prod deploy of \`${refName}\`.`, '', `- Commit: \`${sha}\``, `- versionName: \`${versionName}\``, `- versionCode: \`${versionCode}\``, '- Application id: `ai.hesychia.chat`', '', `Upload \`${assetName}\` to Play Console.`, ].join('\n'); const existing = await api(`/releases/tags/${encodeURIComponent(tag)}`); let release = existing.payload; if (existing.status === 404) { const created = await api('/releases', { method: 'POST', json: { tag_name: tag, target_commitish: sha, name: `Android ${versionName} (${versionCode})`, body, draft: false, prerelease: false, }, }); if (created.status < 200 || created.status >= 300) { throw new Error(`Create release failed (${created.status}): ${JSON.stringify(created.payload)}`); } release = created.payload; console.log(`Created ${tag} at ${releaseUrl(release)}`); } else if (existing.status < 200 || existing.status >= 300) { throw new Error(`Lookup ${tag} failed (${existing.status}): ${JSON.stringify(existing.payload)}`); } else if (release.target_commitish !== sha) { console.error( `${tag} already points at ${release.target_commitish}, not ${sha}. Refusing to replace that Play version.`, ); process.exit(1); } else { console.log(`${tag} already exists for ${sha}`); } if (hasAsset(release)) { console.log(`${assetName} already attached: ${releaseUrl(release)}`); } else { await uploadAsset(release.id); console.log(`Attached ${assetName} to ${releaseUrl(release)}`); }