14 Commits

Author SHA1 Message Date
Jack Boswell
9cf8fd30c9 Update documentation to link to Google's track name information (#191) 2023-09-19 16:44:00 +12:00
Jack Boswell
101c2cddf9 Update all dependencies and bump version (#192) 2023-09-19 16:43:48 +12:00
dependabot[bot]
d17e3fc745 Bump word-wrap from 1.2.3 to 1.2.4 (#184)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-19 10:14:15 +12:00
Jack Boswell
70c7397e86 Fix deprecated releaseFile input not being considered (#174) 2023-02-11 12:10:44 +13:00
Jack Boswell
853233422d Fix a bug where our new timeout caused the action to stay "alive" (#173) 2023-02-11 12:06:30 +13:00
Jack Boswell
9a5e7fcdd5 Add an upload timeout of 60 minutes (#171)
* Add an upload timeout of 60 minutes

* Remove now defunct tests
2023-02-10 22:59:20 +13:00
Sindre Moen
02c72d76fe Remove status default from README (#164)
* Remove status default from README

* Default status to `completed`
2023-02-10 22:00:07 +13:00
Shi Chen
cd131e853c feature: Add support for App Bundle Explorer's Internal app sharing link. (#166)
Updated json5 dependency to patch vulnerability.
2023-01-13 16:55:12 +13:00
Saalihou Ndiaye
8a353d6e46 Add doc about packageName needing to exist first (#168) 2023-01-13 15:31:31 +13:00
dependabot[bot]
d5cf6e9445 Bump json5 from 2.2.1 to 2.2.3 (#167)
Bumps [json5](https://github.com/json5/json5) from 2.2.1 to 2.2.3.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.2.1...v2.2.3)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-13 15:25:47 +13:00
Ivan Milisavljevic
b9dc4464a6 Dont fail the action if optional files are not present on the dics (#162) 2022-12-07 15:19:47 +13:00
Keisuke Yamaguchi
974d2d7566 Update userFraction reuqired to false in README (#157)
* Update userFraction reuqired to false

* Update userFraction to exclusive-exclusive and delete default value
2022-11-23 17:13:43 +13:00
Jack Boswell
536f822b60 Update release workflow (#155) 2022-11-03 19:22:11 +13:00
Jack Boswell
d7e6e42414 fix: Bring back the default release status (#154)
* Bring back the default release status

* Create a test for the bare minimum inputs
2022-11-03 19:11:20 +13:00
9 changed files with 1711 additions and 1752 deletions

View File

@@ -1,24 +1,16 @@
name: "Tag & Deploy Action" name: "Update v1 tag"
on: on:
push: release:
branches: types: [released]
- master
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v2.5.0
- name: Install - uses: rickstaa/action-create-tag@v1
run: npm ci with:
tag: v1
- name: Run Tests force_push_tag: true
run: npm run test
- name: Update v1 Tag
uses: hole19/git-tag-action@master
env:
TAG: v1
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -8,12 +8,12 @@ This action will help you upload an Android `.apk` or `.aab` (Android App Bundle
| --- | --- | --- | --- | | --- | --- | --- | --- |
| releaseFiles | The Android release file(s) to upload (.apk or .aab) | Comma-separated paths. Supports glob via [fast-glob](https://github.com/mrmlnc/fast-glob) | true | | releaseFiles | The Android release file(s) to upload (.apk or .aab) | Comma-separated paths. Supports glob via [fast-glob](https://github.com/mrmlnc/fast-glob) | true |
| serviceAccountJsonPlainText | The service account json in plain text, provided via a secret, etc | The contents of your `service-account.json` | true (or serviceAccountJson) | | serviceAccountJsonPlainText | The service account json in plain text, provided via a secret, etc | The contents of your `service-account.json` | true (or serviceAccountJson) |
| packageName | The package name, or Application Id, of the app you are uploading | A valid package name, e.g. `com.example.myapp` | true | | packageName | The package name, or Application Id, of the app you are uploading | A valid package name, e.g. `com.example.myapp`. The packageName must already exist in the play console account, so make sure you upload a manual apk or aab first through the console | true |
| track | The track in which you want to assign the uploaded app. Defaults to `production` | One of `production`, `beta`, `alpha`, `internalsharing`, `internal`, or a custom track name (case sensitive) | true | | track | The track in which you want to assign the uploaded app. Defaults to `production` | See [APKs and Tracks](https://developers.google.com/android-publisher/tracks#adding_and_modifying_apks) documentation for calculating your target track name | true |
| releaseName | The release name. Not required to be unique. Default is configured by Google Play Console | A user-friendly update name, e.g. `v1.0.0` | false | | releaseName | The release name. Not required to be unique. Default is configured by Google Play Console | A user-friendly update name, e.g. `v1.0.0` | false |
| inAppUpdatePriority | In-app update priority of the release. All newly added APKs in the release will be considered at this priority. Defaults to `0` | `[0-5]`, where `5` is the highest priority | false | | inAppUpdatePriority | In-app update priority of the release. All newly added APKs in the release will be considered at this priority. Defaults to `0` | `[0-5]`, where `5` is the highest priority | false |
| userFraction | Percentage of users who should get the staged version of the app. Defaults to `1.0` | `[0.0-1.0]` | true | | userFraction | Percentage of users who should get the staged version of the app. | `(0.0-1.0)` | false |
| status | Release status. Defaults to `completed` if targeting 100% rollout, `halted` for 0%, and `inProgress` for anything in between | One of `completed`, `inProgress`, `halted`, `draft` | false | | status | Release status. Defaults to `completed`. | One of `completed`, `inProgress`, `halted`, `draft`. Cannot be null. | 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 | | 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 | | mappingFile | The mapping.txt file used to de-obfuscate your stack traces from crash reports | A path to a valid `mapping.txt` file | false |
| debugSymbols | The native-debug-symbols.zip file or folder that contains your debug symbols | A path to a valid `native-debug-symbols.zip file` file or a folder | false | | debugSymbols | The native-debug-symbols.zip file or folder that contains your debug symbols | A path to a valid `native-debug-symbols.zip file` file or a folder | false |
@@ -48,6 +48,10 @@ with:
debugSymbols: app/intermediates/merged_native_libs/release/out/lib debugSymbols: app/intermediates/merged_native_libs/release/out/lib
``` ```
## FAQ
### I get the error "Package not found"
Make sure you upload an apk or aab manually first by creating a release through the play console.
The `whatsNewDirectory` in this example supplies changelogs for English, German and Japanese The `whatsNewDirectory` in this example supplies changelogs for English, German and Japanese
``` ```

View File

@@ -8,6 +8,17 @@ async function expectRunInitiatesUpload(options: RunOptions) {
expect(result.commands.errors).toContainEqual("The incoming JSON object does not contain a client_email field") expect(result.commands.errors).toContainEqual("The incoming JSON object does not contain a client_email field")
} }
test("correct bare minimum inputs", async () => {
const minimumOptions = RunOptions.create()
.setInputs({
serviceAccountJsonPlainText: "{}",
packageName: "com.package.name",
releaseFiles: "./__tests__/releasefiles/*.aab",
track: "production",
})
await expectRunInitiatesUpload(minimumOptions)
})
test("correct inputs for complete rollout", async () => { test("correct inputs for complete rollout", async () => {
// Run with the bare minimum // Run with the bare minimum
const minimumOptions = RunOptions.create() const minimumOptions = RunOptions.create()
@@ -34,8 +45,8 @@ test("correct inputs for complete rollout", async () => {
changesNotSentForReview: "true", changesNotSentForReview: "true",
existingEditId: "123" existingEditId: "123"
}) })
await expectRunInitiatesUpload(extraOptions) await expectRunInitiatesUpload(extraOptions)
}) })
test("correct inputs for inProgress rollout", async () => { test("correct inputs for inProgress rollout", async () => {
// Run with the bare minimum // Run with the bare minimum
@@ -48,7 +59,7 @@ test("correct inputs for inProgress rollout", async () => {
userFraction: "0.99", userFraction: "0.99",
status: "inProgress", status: "inProgress",
}) })
await expectRunInitiatesUpload(minimumOptions) await expectRunInitiatesUpload(minimumOptions)
// Test with optional extras // Test with optional extras
const extraOptions = RunOptions.create() const extraOptions = RunOptions.create()
@@ -65,7 +76,7 @@ test("correct inputs for inProgress rollout", async () => {
changesNotSentForReview: "true", changesNotSentForReview: "true",
existingEditId: "123" existingEditId: "123"
}) })
await expectRunInitiatesUpload(extraOptions) await expectRunInitiatesUpload(extraOptions)
}) })
test("correct inputs for draft rollout", async () => { test("correct inputs for draft rollout", async () => {
@@ -78,7 +89,7 @@ test("correct inputs for draft rollout", async () => {
track: "production", track: "production",
status: "draft", status: "draft",
}) })
await expectRunInitiatesUpload(minimumOptions) await expectRunInitiatesUpload(minimumOptions)
// Test with optional extras // Test with optional extras
const extraOptions = RunOptions.create() const extraOptions = RunOptions.create()
@@ -94,5 +105,5 @@ test("correct inputs for draft rollout", async () => {
changesNotSentForReview: "true", changesNotSentForReview: "true",
existingEditId: "123" existingEditId: "123"
}) })
await expectRunInitiatesUpload(extraOptions) await expectRunInitiatesUpload(extraOptions)
}) })

View File

@@ -36,7 +36,8 @@ inputs:
required: false required: false
status: status:
description: "Release status. This can be set to 'draft' to complete the release at some other time." description: "Release status. This can be set to 'draft' to complete the release at some other time."
required: false required: true
default: "completed"
whatsNewDirectory: whatsNewDirectory:
description: "The directory of localized whats new files" description: "The directory of localized whats new files"
required: false required: false

File diff suppressed because one or more lines are too long

3267
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "upload-google-play", "name": "upload-google-play",
"version": "1.0.19", "version": "1.1.2",
"private": true, "private": true,
"description": "Upload an Android release (.apk or .aab) to Google Play Console", "description": "Upload an Android release (.apk or .aab) to Google Play Console",
"main": "lib/index.js", "main": "lib/index.js",
@@ -20,21 +20,22 @@
"author": "r0adkll", "author": "r0adkll",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.10.0", "@actions/core": "^1.10.1",
"@googleapis/androidpublisher": "^3.0.0", "@googleapis/androidpublisher": "^8.4.0",
"fast-glob": "^3.2.12", "fast-glob": "^3.3.1",
"jszip": "^3.10.1" "jszip": "^3.10.1",
"p-timeout": "^6.1.2"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.2.0", "@types/jest": "^29.5.5",
"@types/node": "^18.11.4", "@types/node": "^20.6.2",
"@typescript-eslint/eslint-plugin": "^5.40.1", "@typescript-eslint/eslint-plugin": "^6.7.0",
"@typescript-eslint/parser": "^5.40.1", "@typescript-eslint/parser": "^6.7.0",
"@vercel/ncc": "^0.34.0", "@vercel/ncc": "^0.38.0",
"eslint": "^8.26.0", "eslint": "^8.49.0",
"github-action-ts-run-api": "^2.2.2", "github-action-ts-run-api": "^3.0.3",
"jest": "^29.2.1", "jest": "^29.7.0",
"ts-jest": "^29.0.3", "ts-jest": "^29.1.1",
"typescript": "^4.8.4" "typescript": "^5.2.2"
} }
} }

View File

@@ -13,14 +13,13 @@ import Apk = androidpublisher_v3.Schema$Apk;
import Bundle = androidpublisher_v3.Schema$Bundle; import Bundle = androidpublisher_v3.Schema$Bundle;
import Track = androidpublisher_v3.Schema$Track; import Track = androidpublisher_v3.Schema$Track;
import InternalAppSharingArtifact = androidpublisher_v3.Schema$InternalAppSharingArtifact; import InternalAppSharingArtifact = androidpublisher_v3.Schema$InternalAppSharingArtifact;
import { Compute } from "google-auth-library/build/src/auth/computeclient"; import { GoogleAuth } from "google-auth-library/build/src/auth/googleauth"
import { JSONClient } from "google-auth-library/build/src/auth/googleauth"
import { readLocalizedReleaseNotes } from "./whatsnew"; import { readLocalizedReleaseNotes } from "./whatsnew";
const androidPublisher: AndroidPublisher = google.androidpublisher('v3'); const androidPublisher: AndroidPublisher = google.androidpublisher('v3');
export interface EditOptions { export interface EditOptions {
auth: Compute | JSONClient; auth: GoogleAuth;
applicationId: string; applicationId: string;
track: string; track: string;
inAppUpdatePriority: number; inAppUpdatePriority: number;
@@ -52,10 +51,8 @@ export async function runUpload(
scopes: ['https://www.googleapis.com/auth/androidpublisher'] scopes: ['https://www.googleapis.com/auth/androidpublisher']
}); });
const authClient = await auth.getClient();
const result = await uploadToPlayStore({ const result = await uploadToPlayStore({
auth: authClient, auth: auth,
applicationId: packageName, applicationId: packageName,
track: track, track: track,
inAppUpdatePriority: inAppUpdatePriority || 0, inAppUpdatePriority: inAppUpdatePriority || 0,
@@ -75,17 +72,16 @@ export async function runUpload(
} }
async function uploadToPlayStore(options: EditOptions, releaseFiles: string[]): Promise<string | void> { async function uploadToPlayStore(options: EditOptions, releaseFiles: string[]): Promise<string | void> {
const internalSharingDownloadUrls: string[] = []
// Check the 'track' for 'internalsharing', if so switch to a non-track api // Check the 'track' for 'internalsharing', if so switch to a non-track api
if (options.track === 'internalsharing') { if (options.track === 'internalsharing') {
core.debug("Track is Internal app sharing, switch to special upload api") core.debug("Track is Internal app sharing, switch to special upload api")
const downloadUrls: string[] = []
for (const releaseFile of releaseFiles) { for (const releaseFile of releaseFiles) {
core.debug(`Uploading ${releaseFile}`); core.debug(`Uploading ${releaseFile}`);
const url = await uploadInternalSharingRelease(options, releaseFile) const url = await uploadInternalSharingRelease(options, releaseFile)
downloadUrls.push(url) internalSharingDownloadUrls.push(url)
} }
core.setOutput("internalSharingDownloadUrls", downloadUrls);
core.exportVariable("INTERNAL_SHARING_DOWNLOAD_URLS", downloadUrls);
} else { } else {
// Create a new Edit // Create a new Edit
const appEditId = await getOrCreateEdit(options) const appEditId = await getOrCreateEdit(options)
@@ -96,6 +92,14 @@ async function uploadToPlayStore(options: EditOptions, releaseFiles: string[]):
// Upload artifacts to Google Play, and store their version codes // Upload artifacts to Google Play, and store their version codes
const versionCodes = await uploadReleaseFiles(appEditId, options, releaseFiles) const versionCodes = await uploadReleaseFiles(appEditId, options, releaseFiles)
// Infer the download URL from the version codes
for (const versionCode of versionCodes) {
const url = inferInternalSharingDownloadUrl(options.applicationId, versionCode);
core.setOutput("internalSharingDownloadUrl", url);
core.exportVariable("INTERNAL_SHARING_DOWNLOAD_URL", url);
internalSharingDownloadUrls.push(url);
}
// Add the uploaded artifacts to the Edit track // Add the uploaded artifacts to the Edit track
await addReleasesToTrack(appEditId, options, versionCodes); await addReleasesToTrack(appEditId, options, versionCodes);
@@ -117,6 +121,9 @@ async function uploadToPlayStore(options: EditOptions, releaseFiles: string[]):
return Promise.reject(res.status); return Promise.reject(res.status);
} }
} }
core.setOutput("internalSharingDownloadUrls", internalSharingDownloadUrls);
core.exportVariable("INTERNAL_SHARING_DOWNLOAD_URLS", internalSharingDownloadUrls);
} }
async function uploadInternalSharingRelease(options: EditOptions, releaseFile: string): Promise<string> { async function uploadInternalSharingRelease(options: EditOptions, releaseFile: string): Promise<string> {
@@ -398,3 +405,10 @@ async function uploadReleaseFiles(appEditId: string, options: EditOptions, relea
return versionCodes return versionCodes
} }
function inferInternalSharingDownloadUrl(
applicationId: string,
versionCode: number
) {
return `https://play.google.com/apps/test/${applicationId}/${versionCode}`;
}

View File

@@ -1,8 +1,9 @@
import * as core from '@actions/core'; import * as core from '@actions/core'
import * as fs from "fs"; import * as fs from "fs"
import { runUpload } from "./edits"; import { runUpload } from "./edits"
import { validateInAppUpdatePriority, validateReleaseFiles, validateStatus, validateUserFraction } from "./input-validation" import { validateInAppUpdatePriority, validateReleaseFiles, validateStatus, validateUserFraction } from "./input-validation"
import { unlink, writeFile } from 'fs/promises'; import { unlink, writeFile } from 'fs/promises'
import pTimeout from 'p-timeout'
export async function run() { export async function run() {
try { try {
@@ -12,7 +13,7 @@ export async function run() {
const releaseFile = core.getInput('releaseFile', { required: false }); const releaseFile = core.getInput('releaseFile', { required: false });
const releaseFiles = core.getInput('releaseFiles', { required: false }) const releaseFiles = core.getInput('releaseFiles', { required: false })
?.split(',') ?.split(',')
?.filter(x => x !== '') || []; ?.filter(x => x !== '');
const releaseName = core.getInput('releaseName', { required: false }); const releaseName = core.getInput('releaseName', { required: false });
const track = core.getInput('track', { required: true }); const track = core.getInput('track', { required: true });
const inAppUpdatePriority = core.getInput('inAppUpdatePriority', { required: false }); const inAppUpdatePriority = core.getInput('inAppUpdatePriority', { required: false });
@@ -54,33 +55,35 @@ export async function run() {
const validatedReleaseFiles: string[] = await validateReleaseFiles(releaseFiles ?? [releaseFile]) const validatedReleaseFiles: string[] = await validateReleaseFiles(releaseFiles ?? [releaseFile])
if (whatsNewDir != undefined && whatsNewDir.length > 0 && !fs.existsSync(whatsNewDir)) { if (whatsNewDir != undefined && whatsNewDir.length > 0 && !fs.existsSync(whatsNewDir)) {
core.setFailed(`Unable to find 'whatsnew' directory @ ${whatsNewDir}`); core.warning(`Unable to find 'whatsnew' directory @ ${whatsNewDir}`);
return
} }
if (mappingFile != undefined && mappingFile.length > 0 && !fs.existsSync(mappingFile)) { if (mappingFile != undefined && mappingFile.length > 0 && !fs.existsSync(mappingFile)) {
core.setFailed(`Unable to find 'mappingFile' @ ${mappingFile}`); core.warning(`Unable to find 'mappingFile' @ ${mappingFile}`);
return
} }
if (debugSymbols != undefined && debugSymbols.length > 0 && !fs.existsSync(debugSymbols)) { if (debugSymbols != undefined && debugSymbols.length > 0 && !fs.existsSync(debugSymbols)) {
core.setFailed(`Unable to find 'debugSymbols' @ ${debugSymbols}`); core.warning(`Unable to find 'debugSymbols' @ ${debugSymbols}`);
return
} }
await runUpload( await pTimeout(
packageName, runUpload(
track, packageName,
inAppUpdatePriorityInt, track,
userFractionFloat, inAppUpdatePriorityInt,
whatsNewDir, userFractionFloat,
mappingFile, whatsNewDir,
debugSymbols, mappingFile,
releaseName, debugSymbols,
changesNotSentForReview, releaseName,
existingEditId, changesNotSentForReview,
status, existingEditId,
validatedReleaseFiles status,
validatedReleaseFiles
),
{
milliseconds: 3.6e+6
}
) )
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {