4 Commits

Author SHA1 Message Date
Jack Boswell
7406d2c7a1 Update deps (#137) 2022-09-11 19:51:48 +12:00
Jack Boswell
623184c55d Validate library is up to date in workflow (#121) 2022-06-20 21:17:01 +12:00
Jack Boswell
91c8f42cf7 feature: Allow editing existing Edits (#120)
* Only take edit ID for track validation

* Only take edit ID for adding releases

* Only take edit ID for mapping file upload

* Only take edit ID for uploading artifacts

* Only use app edit ID for uploading releases

* Rebuild

* Allow passing in an existing edit ID

* Expose existingEditId

* Run build
2022-06-20 20:59:52 +12:00
Jack Boswell
e00bb8ce1e feature: Add support for changesNotSentForReview (#117)
Credits to @princefr for the original implementation
2022-06-18 19:50:39 +12:00
8 changed files with 1301 additions and 1185 deletions

View File

@@ -18,13 +18,25 @@ jobs:
steps:
- uses: actions/checkout@v3
# Runs a single command using the runners shell
# Set up NPM and deps
- name: Set up NPM
run: npm ci
# Runs a set of commands using the runners shell
# Do a build
- name: Run Build
run: npm run build
# Check whether the compiled library changed
- name: Check Build Updated
uses: tj-actions/verify-changed-files@v9.2
id: verify-built-lib
with:
files: lib/index.js
# If it did change, the authro forgot to build. Fail the action
- name: Verify Build Updated
if: steps.verify-changed-files.outputs.files_changed == 'true'
run: exit 1
- name: Run Tests
run: npm test

View File

@@ -16,7 +16,9 @@ This action will help you upload an Android `.apk` or `.aab` (Android App Bundle
| status | Release status. This can be set to `draft` to complete the release at some other time. Defaults to `completed` if targeting 100% rollout, else `inProgress` | One of `completed`, `inProgress`, `halted`, `draft` | false |
| whatsNewDirectory | The directory of localized "whats new" files to upload as the release notes. The files contained in the `whatsNewDirectory` MUST use the pattern `whatsnew-<LOCALE>` where `LOCALE` is using the [`BCP 47`](https://tools.ietf.org/html/bcp47) format | A path to a valid `whatsNewDirectory` | false |
| mappingFile | The mapping.txt file used to de-obfuscate your stack traces from crash reports | A path to a valid `mapping.txt` file | false |
| changesNotSentForReview | Indicates that the changes in this edit will not be reviewed until they are explicitly sent for review from the Google Play Console. Defaults to `false` | `true` or `false` | `false` |
| serviceAccountJson | The service account json private key file to authorize the upload request. Can be used instead of `serviceAccountJsonPlainText` to specify a file rather than provide a secret | A path to a valid `service-account.json` file | true (or serviceAccountJsonPlainText) |
| existingEditId | The ID of an existing edit that has not been completed. If this is supplied, the action will append information to that rather than creating an edit | A valid, unpublished Edit ID | false |
| ~~releaseFile~~ | Please switch to using `releaseFiles` as this will be removed in the future | | false |
## Outputs

View File

@@ -43,6 +43,13 @@ inputs:
mappingFile:
description: "The mapping.txt file used to de-obfuscate your stack traces from crash reports"
required: false
changesNotSentForReview:
description: "Indicates that the changes in this edit will not be reviewed until they are explicitly sent for review from the Google Play Console"
default: false
required: false
existingEditId:
description: "The ID of an existing edit that has not been completed. If this is supplied, the action will append information to that rather than creating an edit"
required: false
outputs:
internalSharingDownloadUrl:
description: "The internal app sharing download url if track was 'internalsharing'"

File diff suppressed because one or more lines are too long

2367
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,16 +20,16 @@
"author": "r0adkll",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.9.0",
"@actions/core": "^1.9.1",
"@googleapis/androidpublisher": "^3.0.0",
"fast-glob": "^3.2.5"
"fast-glob": "^3.2.12"
},
"devDependencies": {
"@types/jest": "^28.1.2",
"@types/node": "^18.0.0",
"@types/jest": "^29.0.0",
"@types/node": "^18.7.16",
"@vercel/ncc": "^0.34.0",
"jest": "^28.1.1",
"ts-jest": "^28.0.5",
"typescript": "^4.7.4"
"jest": "^29.0.3",
"ts-jest": "^29.0.0",
"typescript": "^4.8.3"
}
}

View File

@@ -1,19 +1,18 @@
import * as core from '@actions/core';
import * as fs from "fs";
import {readFileSync} from "fs";
import { readFileSync } from "fs";
import * as google from '@googleapis/androidpublisher';
import {androidpublisher_v3} from "@googleapis/androidpublisher";
import { androidpublisher_v3 } from "@googleapis/androidpublisher";
import AndroidPublisher = androidpublisher_v3.Androidpublisher;
import AppEdit = androidpublisher_v3.Schema$AppEdit;
import Apk = androidpublisher_v3.Schema$Apk;
import Bundle = androidpublisher_v3.Schema$Bundle;
import Track = androidpublisher_v3.Schema$Track;
import InternalAppSharingArtifact = androidpublisher_v3.Schema$InternalAppSharingArtifact;
import {Compute} from "google-auth-library/build/src/auth/computeclient";
import {JSONClient} from "google-auth-library/build/src/auth/googleauth"
import {readLocalizedReleaseNotes} from "./whatsnew";
import { Compute } from "google-auth-library/build/src/auth/computeclient";
import { JSONClient } from "google-auth-library/build/src/auth/googleauth"
import { readLocalizedReleaseNotes } from "./whatsnew";
const androidPublisher: AndroidPublisher = google.androidpublisher('v3');
@@ -27,6 +26,8 @@ export interface EditOptions {
mappingFile?: string;
name?: string;
status?: string;
changesNotSentForReview?: boolean;
existingEditId?: string;
}
export async function uploadToPlayStore(options: EditOptions, releaseFiles: string[]): Promise<string | undefined> {
@@ -50,14 +51,14 @@ export async function uploadToPlayStore(options: EditOptions, releaseFiles: stri
} else {
// Create a new Edit
core.info(`Creating a new Edit for this release`)
const appEdit = await androidPublisher.edits.insert({
const appEditId = options.existingEditId || (await androidPublisher.edits.insert({
auth: options.auth,
packageName: options.applicationId
});
})).data.id
// Validate the given track
core.info(`Validating track '${options.track}'`)
await validateSelectedTrack(appEdit.data, options).catch(reason => {
await validateSelectedTrack(appEditId!, options).catch(reason => {
core.setFailed(reason);
return Promise.reject(reason);
});
@@ -66,7 +67,7 @@ export async function uploadToPlayStore(options: EditOptions, releaseFiles: stri
const versionCodes = new Array<number>();
for (const releaseFile of releaseFiles) {
core.info(`Uploading ${releaseFile}`);
const versionCode = await uploadRelease(appEdit.data, options, releaseFile).catch(reason => {
const versionCode = await uploadRelease(appEditId!, options, releaseFile).catch(reason => {
core.setFailed(reason);
return Promise.reject(reason);
});
@@ -76,15 +77,16 @@ export async function uploadToPlayStore(options: EditOptions, releaseFiles: stri
// Add the uploaded artifacts to the Edit track
core.info(`Adding ${versionCodes.length} artifacts to release on '${options.track}' track`)
const track = await addReleasesToTrack(appEdit.data, options, versionCodes);
const track = await addReleasesToTrack(appEditId!, options, versionCodes);
core.debug(`Track: ${track}`);
// Commit the pending Edit
core.info(`Committing the Edit`)
const res = await androidPublisher.edits.commit({
auth: options.auth,
editId: appEdit.data.id!,
packageName: options.applicationId
editId: appEditId!,
packageName: options.applicationId,
changesNotSentForReview: options.changesNotSentForReview
});
// Simple check to see whether commit was successful
@@ -116,24 +118,24 @@ async function uploadInternalSharingRelease(options: EditOptions, releaseFile: s
}
}
async function uploadRelease(appEdit: AppEdit, options: EditOptions, releaseFile: string): Promise<number | undefined | null> {
async function uploadRelease(appEditId: string, options: EditOptions, releaseFile: string): Promise<number | undefined | null> {
if (releaseFile.endsWith('.apk')) {
const apk = await uploadApk(appEdit, options, releaseFile);
await uploadMappingFile(appEdit, apk.versionCode!, options);
const apk = await uploadApk(appEditId, options, releaseFile);
await uploadMappingFile(appEditId, apk.versionCode!, options);
return Promise.resolve(apk.versionCode);
} else if (releaseFile.endsWith('.aab')) {
const bundle = await uploadBundle(appEdit, options, releaseFile);
await uploadMappingFile(appEdit, bundle.versionCode!, options);
const bundle = await uploadBundle(appEditId, options, releaseFile);
await uploadMappingFile(appEditId, bundle.versionCode!, options);
return Promise.resolve(bundle.versionCode);
} else {
return Promise.reject(`${releaseFile} is invalid`);
}
}
async function validateSelectedTrack(appEdit: AppEdit, options: EditOptions): Promise<undefined> {
async function validateSelectedTrack(appEditId: string, options: EditOptions): Promise<undefined> {
const res = await androidPublisher.edits.tracks.list({
auth: options.auth,
editId: appEdit.id!,
editId: appEditId,
packageName: options.applicationId
});
const allTracks = res.data.tracks;
@@ -142,7 +144,7 @@ async function validateSelectedTrack(appEdit: AppEdit, options: EditOptions): Pr
}
}
async function addReleasesToTrack(appEdit: AppEdit, options: EditOptions, versionCodes: number[]): Promise<Track> {
async function addReleasesToTrack(appEditId: string, options: EditOptions, versionCodes: number[]): Promise<Track> {
let status: string | undefined = options.status;
if (!status) {
if (options.userFraction != undefined) {
@@ -152,11 +154,11 @@ async function addReleasesToTrack(appEdit: AppEdit, options: EditOptions, versio
}
}
core.debug(`Creating Track Release for Edit(${appEdit.id}) for Track(${options.track}) with a UserFraction(${options.userFraction}), Status(${status}), and VersionCodes(${versionCodes})`);
core.debug(`Creating Track Release for Edit(${appEditId}) for Track(${options.track}) with a UserFraction(${options.userFraction}), Status(${status}), and VersionCodes(${versionCodes})`);
const res = await androidPublisher.edits.tracks
.update({
auth: options.auth,
editId: appEdit.id!,
editId: appEditId,
packageName: options.applicationId,
track: options.track,
requestBody: {
@@ -177,15 +179,15 @@ async function addReleasesToTrack(appEdit: AppEdit, options: EditOptions, versio
return res.data;
}
async function uploadMappingFile(appEdit: AppEdit, versionCode: number, options: EditOptions) {
async function uploadMappingFile(appEditId: string, versionCode: number, options: EditOptions) {
if (options.mappingFile != undefined && options.mappingFile.length > 0) {
const mapping = readFileSync(options.mappingFile, 'utf-8');
if (mapping != undefined) {
core.debug(`[${appEdit.id}, versionCode=${versionCode}, packageName=${options.applicationId}]: Uploading Proguard mapping file @ ${options.mappingFile}`);
core.debug(`[${appEditId}, versionCode=${versionCode}, packageName=${options.applicationId}]: Uploading Proguard mapping file @ ${options.mappingFile}`);
await androidPublisher.edits.deobfuscationfiles.upload({
auth: options.auth,
packageName: options.applicationId,
editId: appEdit.id!,
editId: appEditId,
apkVersionCode: versionCode,
deobfuscationFileType: 'proguard',
media: {
@@ -227,13 +229,13 @@ async function internalSharingUploadBundle(options: EditOptions, bundleReleaseFi
return res.data;
}
async function uploadApk(appEdit: AppEdit, options: EditOptions, apkReleaseFile: string): Promise<Apk> {
core.debug(`[${appEdit.id}, packageName=${options.applicationId}]: Uploading APK @ ${apkReleaseFile}`);
async function uploadApk(appEditId: string, options: EditOptions, apkReleaseFile: string): Promise<Apk> {
core.debug(`[${appEditId}, packageName=${options.applicationId}]: Uploading APK @ ${apkReleaseFile}`);
const res = await androidPublisher.edits.apks.upload({
auth: options.auth,
packageName: options.applicationId,
editId: appEdit.id!,
editId: appEditId,
media: {
mimeType: 'application/vnd.android.package-archive',
body: fs.createReadStream(apkReleaseFile)
@@ -243,12 +245,12 @@ async function uploadApk(appEdit: AppEdit, options: EditOptions, apkReleaseFile:
return res.data
}
async function uploadBundle(appEdit: AppEdit, options: EditOptions, bundleReleaseFile: string): Promise<Bundle> {
core.debug(`[${appEdit.id}, packageName=${options.applicationId}]: Uploading App Bundle @ ${bundleReleaseFile}`);
async function uploadBundle(appEditId: string, options: EditOptions, bundleReleaseFile: string): Promise<Bundle> {
core.debug(`[${appEditId}, packageName=${options.applicationId}]: Uploading App Bundle @ ${bundleReleaseFile}`);
const res = await androidPublisher.edits.bundles.upload({
auth: options.auth,
packageName: options.applicationId,
editId: appEdit.id!,
editId: appEditId,
media: {
mimeType: 'application/octet-stream',
body: fs.createReadStream(bundleReleaseFile)

View File

@@ -24,6 +24,8 @@ async function run() {
const status = core.getInput('status', { required: false });
const whatsNewDir = core.getInput('whatsNewDirectory', { required: false });
const mappingFile = core.getInput('mappingFile', { required: false });
const changesNotSentForReview = core.getInput('changesNotSentForReview', { required: false }) == 'true';
const existingEditId = core.getInput('existingEditId')
// Validate that we have a service account json in some format
if (!serviceAccountJson && !serviceAccountJsonRaw) {
@@ -116,7 +118,9 @@ async function run() {
status: status,
whatsNewDir: whatsNewDir,
mappingFile: mappingFile,
name: releaseName
name: releaseName,
changesNotSentForReview: changesNotSentForReview,
existingEditId: existingEditId
}, validatedReleaseFiles);
console.log(`Finished uploading to the Play Store: ${result}`)