26 Commits

Author SHA1 Message Date
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
Jack Boswell (boswelja)
78c9e796b1 Update dependencies + prepare for release 2022-10-24 08:17:46 +00:00
Jack Boswell
980a5c4201 Write edit expiry time to debug console (#152) 2022-10-24 21:11:26 +13:00
Jack Boswell
81a2f1ad1c Print track names when invalid track provided (#151)
* Print track names when invalid track provided

* Remove redundant safe call
2022-10-24 21:06:38 +13:00
Jack Boswell
b84eace68d Set up full input testing (#150)
* Extract main loop to something testable

* Revert "Extract main loop to something testable"

This reverts commit 35a78459698fe267933c554283212c2d06ed4bf0.

* Extract code that interacts with Google Play
We can now test our input flow as a whole

* Create some basic tests for `completed` status publishing

* Fix applying inputs
We don't want to be keeping values between runs

* Drop some redundant tests

* Create basic tests for draft rollout

* Run build

* Fix userFraction bug

* Improve readability of initialising inputs

* Fix runUpload mocking

* Migrate to github-action-ts-run-api
2022-10-24 14:18:11 +13:00
Jack Boswell (boswelja)
4d510d380a Attempt at fixing status validation
Our validation tests are A-OK, but inputs are not :(
2022-09-26 15:37:05 +13:00
Jack Boswell
99b7a1e1d4 Fix issues parsing Int and Float (#145)
Undefined was transformed into NaN, which is not undefined and thus failed validation
2022-09-24 16:51:04 +12:00
Jack Boswell
7cd1379b4f refactor: Centralized input validation (#142)
* Initial implementaion of service account json, user fraction and status validation

* Move validation to input-validation
Also created some more validators

* Rearrange whatsnew tests to clear out test folder

* Return error messages for input validation

* Create tests for some validation code

* Separate user fraction and status validation

* Write more tests and fix some bugs

* Refactor to use promises

* Fix release file loading from glob
2022-09-21 17:15:54 +12:00
Jack Boswell
b028ed83b3 Set up ESLint (#138)
* Add eslint and config

* Refactor edits.ts to comply with eslint

* Refactor whatsnew.ts to comply with eslint

* Fix final ESLint issues

* Run build
2022-09-12 20:37:51 +12:00
Jack Boswell
5427bcbe2b Ignore userFraction if status is draft (#140) 2022-09-12 20:23:41 +12:00
Thomas Aunvik
6a74f079de Add Debug Symbol File/Folder Upload (#130)
* Add Debug Symbol File/Folder Upload

Uploads Deobfuscation File type 'nativeCode'.

Inputs allowed in 'debugSymbols', a .zip file, or a folder that contains the symbols.

* Space Formatting

* Zip Directory Function higher level, and formatting
2022-09-12 00:21:02 +12:00
Jack Boswell
c16321c553 feature: Infer status from provided parameters (#122)
* Mark status as deprecated

* Make userFraction required & provide a default
We'll use this to infer `inProgress` or `completed`

* Infer status from userFraction

* Fix workflow

* Update README

* Run build

* Add support for drafts again

* Pass in isDraft

* Warn about setting draft and userFraction simultaneously

* Expose isDraft

* Do a build

* Allow passing status in again
This will be required for draft/halted
It is optional though, if it's not specified, we have updated logic for inferring it

* Update userFraction description

* Revert "Fix workflow"

This reverts commit ad98b3d449f0f36c9d4f0a3d0b2eb49a4c3e1aa9.

* Remove deprecated tag from status

* Move status back

* Allow 0 userFraction

* Run build
2022-09-11 20:53:18 +12:00
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
22 changed files with 4893 additions and 1795 deletions

15
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,15 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
],
ignorePatterns: ["lib/*.js", "jest.config.js", "__tests__/**"]
};

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

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

View File

@@ -8,15 +8,18 @@ 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 |
| 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 |
| 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 |
| userFraction | Percentage of users who should get the staged version of the app. Defaults to `1.0` | `(0.0-1.0)` | false |
| 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 |
| userFraction | Percentage of users who should get the staged version of the app. | `(0.0-1.0)` | 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 |
| 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 |
| 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
@@ -42,8 +45,13 @@ with:
userFraction: 0.33
whatsNewDirectory: distribution/whatsnew
mappingFile: app/build/outputs/mapping/release/mapping.txt
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
```

View File

@@ -0,0 +1,74 @@
import { validateInAppUpdatePriority, validateReleaseFiles, validateStatus, validateUserFraction } from "../src/input-validation"
test("invalid in-app update priority fails validation", async () => {
const testValues = [-1, 6, -1000, 1000]
for (const value of testValues) {
await expect(validateInAppUpdatePriority(value)).rejects.toThrowError()
}
})
test("valid in-app update priority passes validation", async () => {
const testValues = [0, 1, 2, 3, 4, 5, undefined]
for (const value of testValues) {
await validateInAppUpdatePriority(value)
}
})
test("invalid releaseFiles glob fails validation", async () => {
const testValues = ['./__tests__/releasefiles/*.null', `./__tests__/releasefiles/nonexistent-release.aab`]
await expect(validateReleaseFiles(testValues)).rejects.toThrowError()
})
test("valid releaseFiles glob passes validation", async () => {
const testValues = ['./__tests__/releasefiles/*.aab', `./__tests__/releasefiles/release.aab`]
await validateReleaseFiles(testValues)
})
test("fractionless status without fraction passes validation", async () => {
const testValues = ['completed', 'draft']
for (const value of testValues) {
await validateStatus(value, false)
}
})
test("fractionless status with fraction fails validation", async () => {
const testValues = ['completed', 'draft']
for (const value of testValues) {
await expect(validateStatus(value, true)).rejects.toThrowError()
}
})
test("fractioned status without fraction fails validation", async () => {
const testValues = ['inProgress', 'halted']
for (const value of testValues) {
await expect(validateStatus(value, false)).rejects.toThrowError()
}
})
test("fractioned status with fraction passes validation", async () => {
const testValues = ['inProgress', 'halted']
for (const value of testValues) {
await validateStatus(value, true)
}
})
test("invalid status fails validation", async () => {
const testValues = ['statusGoBrrr', undefined]
for (const value of testValues) {
await expect(validateStatus(value, true)).rejects.toThrowError()
}
})
test("invalid user fraction fails validation", async () => {
const testValues = [0, 1, -1, 2, NaN]
for (const value of testValues) {
await expect(validateUserFraction(value)).rejects.toThrowError()
}
})
test("valid user fraction passes validation", async () => {
const testValues = [0.1, 0.9, 0.5, undefined]
for (const value of testValues) {
await validateUserFraction(value)
}
})

109
__tests__/main.test.ts Normal file
View File

@@ -0,0 +1,109 @@
import {RunOptions, RunTarget} from 'github-action-ts-run-api';
const target = RunTarget.mainJs('action.yml');
async function expectRunInitiatesUpload(options: RunOptions) {
const result = await target.run(options)
expect(result.commands.errors.length).toBe(1)
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 () => {
// Run with the bare minimum
const minimumOptions = RunOptions.create()
.setInputs({
serviceAccountJsonPlainText: "{}",
packageName: "com.package.name",
releaseFiles: "./__tests__/releasefiles/*.aab",
track: "production",
status: "completed",
})
await expectRunInitiatesUpload(minimumOptions)
// Test with optional extras
const extraOptions = RunOptions.create()
.setInputs({
serviceAccountJsonPlainText: "{}",
packageName: "com.package.name",
releaseFiles: "./__tests__/releasefiles/*.aab",
releaseName: "Release name",
track: "production",
inAppUpdatePriority: "3",
status: "completed",
whatsNewDir: "./__tests__/whatsnew",
changesNotSentForReview: "true",
existingEditId: "123"
})
await expectRunInitiatesUpload(extraOptions)
})
test("correct inputs for inProgress rollout", async () => {
// Run with the bare minimum
const minimumOptions = RunOptions.create()
.setInputs({
serviceAccountJsonPlainText: "{}",
packageName: "com.package.name",
releaseFiles: "./__tests__/releasefiles/*.aab",
track: "production",
userFraction: "0.99",
status: "inProgress",
})
await expectRunInitiatesUpload(minimumOptions)
// Test with optional extras
const extraOptions = RunOptions.create()
.setInputs({
serviceAccountJsonPlainText: "{}",
packageName: "com.package.name",
releaseFiles: "./__tests__/releasefiles/*.aab",
releaseName: "Release name",
track: "production",
inAppUpdatePriority: "3",
userFraction: "0.5",
status: "inProgress",
whatsNewDir: "./__tests__/whatsnew",
changesNotSentForReview: "true",
existingEditId: "123"
})
await expectRunInitiatesUpload(extraOptions)
})
test("correct inputs for draft rollout", async () => {
// Run with the bare minimum
const minimumOptions = RunOptions.create()
.setInputs({
serviceAccountJsonPlainText: "{}",
packageName: "com.package.name",
releaseFiles: "./__tests__/releasefiles/*.aab",
track: "production",
status: "draft",
})
await expectRunInitiatesUpload(minimumOptions)
// Test with optional extras
const extraOptions = RunOptions.create()
.setInputs({
serviceAccountJsonPlainText: "{}",
packageName: "com.package.name",
releaseFiles: "./__tests__/releasefiles/*.aab",
releaseName: "Release name",
track: "production",
inAppUpdatePriority: "3",
status: "draft",
whatsNewDir: "./__tests__/whatsnew",
changesNotSentForReview: "true",
existingEditId: "123"
})
await expectRunInitiatesUpload(extraOptions)
})

View File

View File

@@ -1,12 +1,7 @@
import * as process from 'process'
import * as cp from 'child_process'
import * as path from 'path'
import {readLocalizedReleaseNotes} from "../src/whatsnew";
import {androidpublisher_v3} from "@googleapis/androidpublisher";
import LocalizedText = androidpublisher_v3.Schema$LocalizedText;
test("read localized whatsnew files", async () => {
let texts = await readLocalizedReleaseNotes("./__tests__");
let texts = await readLocalizedReleaseNotes("./__tests__/whatsnew");
expect(texts).toHaveLength(6);
expect(texts).toContainEqual({
language: "en-US",
@@ -32,4 +27,4 @@ test("read localized whatsnew files", async () => {
language: "et",
text: "test_changelog_file_estonian"
});
});
});

View File

@@ -36,13 +36,24 @@ inputs:
required: false
status:
description: "Release status. This can be set to 'draft' to complete the release at some other time."
required: false
required: true
default: "completed"
whatsNewDirectory:
description: "The directory of localized whats new files"
required: false
mappingFile:
description: "The mapping.txt file used to de-obfuscate your stack traces from crash reports"
required: false
debugSymbols:
description: "The native-debug-symbols.zip file or folder that contains your debug symbols"
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

5771
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "upload-google-play",
"version": "1.0.17",
"version": "1.1.1",
"private": true,
"description": "Upload an Android release (.apk or .aab) to Google Play Console",
"main": "lib/index.js",
@@ -20,16 +20,22 @@
"author": "r0adkll",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.9.0",
"@googleapis/androidpublisher": "^3.0.0",
"fast-glob": "^3.2.5"
"@actions/core": "^1.10.0",
"@googleapis/androidpublisher": "^4.1.0",
"fast-glob": "^3.2.12",
"jszip": "^3.10.1",
"p-timeout": "^6.1.0"
},
"devDependencies": {
"@types/jest": "^28.1.2",
"@types/node": "^18.0.0",
"@vercel/ncc": "^0.34.0",
"jest": "^28.1.1",
"ts-jest": "^28.0.5",
"typescript": "^4.7.4"
"@types/jest": "^29.2.0",
"@types/node": "^18.11.4",
"@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1",
"@vercel/ncc": "^0.36.1",
"eslint": "^8.26.0",
"github-action-ts-run-api": "^2.2.2",
"jest": "^29.2.1",
"ts-jest": "^29.0.3",
"typescript": "^4.8.4"
}
}

View File

@@ -1,19 +1,21 @@
import * as core from '@actions/core';
import * as fs from "fs";
import {readFileSync} from "fs";
import { readFileSync, lstatSync } from "fs";
import JSZip from 'jszip';
import path = require('path');
import { Readable } from 'stream';
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');
@@ -25,138 +27,168 @@ export interface EditOptions {
userFraction?: number;
whatsNewDir?: string;
mappingFile?: string;
debugSymbols?: string;
name?: string;
status?: string;
status: string;
changesNotSentForReview?: boolean;
existingEditId?: string;
}
export async function uploadToPlayStore(options: EditOptions, releaseFiles: string[]): Promise<string | undefined> {
export async function runUpload(
packageName: string,
track: string,
inAppUpdatePriority: number | undefined,
userFraction: number | undefined,
whatsNewDir: string | undefined,
mappingFile: string | undefined,
debugSymbols: string | undefined,
name: string | undefined,
changesNotSentForReview: boolean,
existingEditId: string | undefined,
status: string,
validatedReleaseFiles: string[]
) {
const auth = new google.auth.GoogleAuth({
scopes: ['https://www.googleapis.com/auth/androidpublisher']
});
const authClient = await auth.getClient();
const result = await uploadToPlayStore({
auth: authClient,
applicationId: packageName,
track: track,
inAppUpdatePriority: inAppUpdatePriority || 0,
userFraction: userFraction,
whatsNewDir: whatsNewDir,
mappingFile: mappingFile,
debugSymbols: debugSymbols,
name: name,
changesNotSentForReview: changesNotSentForReview,
existingEditId: existingEditId,
status: status
}, validatedReleaseFiles);
if (result) {
console.log(`Finished uploading to the Play Store: ${result}`)
}
}
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
if (options.track === 'internalsharing') {
core.debug("Track is Internal app sharing, switch to special upload api")
let downloadUrls: string[] = []
for (const releaseFile of releaseFiles) {
core.debug(`Uploading ${releaseFile}`);
await uploadInternalSharingRelease(options, releaseFile)
.then(downloadUrl => {
downloadUrls.push(downloadUrl as string)
})
.catch(reason => {
core.setFailed(reason);
return Promise.reject(reason);
});
const url = await uploadInternalSharingRelease(options, releaseFile)
internalSharingDownloadUrls.push(url)
}
core.setOutput("internalSharingDownloadUrls", downloadUrls);
core.exportVariable("INTERNAL_SHARING_DOWNLOAD_URLS", downloadUrls);
} else {
// Create a new Edit
core.info(`Creating a new Edit for this release`)
const appEdit = await androidPublisher.edits.insert({
auth: options.auth,
packageName: options.applicationId
});
const appEditId = await getOrCreateEdit(options)
// Validate the given track
core.info(`Validating track '${options.track}'`)
await validateSelectedTrack(appEdit.data, options).catch(reason => {
core.setFailed(reason);
return Promise.reject(reason);
});
await validateSelectedTrack(appEditId, options)
// Upload artifacts to Google Play, and store their version codes
const versionCodes = new Array<number>();
for (const releaseFile of releaseFiles) {
core.info(`Uploading ${releaseFile}`);
const versionCode = await uploadRelease(appEdit.data, options, releaseFile).catch(reason => {
core.setFailed(reason);
return Promise.reject(reason);
});
versionCodes.push(versionCode!);
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);
}
core.info(`Successfully uploaded ${versionCodes.length} artifacts`)
// 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);
core.debug(`Track: ${track}`);
await addReleasesToTrack(appEditId, options, versionCodes);
// 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
if (res.data.id != null) {
if (res.data.id) {
core.info(`Successfully committed ${res.data.id}`);
return Promise.resolve(res.data.id!);
return res.data.id
} else {
core.setFailed(`Error ${res.status}: ${res.statusText}`);
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 | undefined | null> {
async function uploadInternalSharingRelease(options: EditOptions, releaseFile: string): Promise<string> {
let res: google.androidpublisher_v3.Schema$InternalAppSharingArtifact
if (releaseFile.endsWith('.apk')) {
const res = await internalSharingUploadApk(options, releaseFile)
core.setOutput("internalSharingDownloadUrl", res.downloadUrl);
core.exportVariable("INTERNAL_SHARING_DOWNLOAD_URL", res.downloadUrl);
console.log(`${releaseFile} uploaded to Internal Sharing, download it with ${res.downloadUrl}`)
return Promise.resolve(res.downloadUrl)
res = await internalSharingUploadApk(options, releaseFile)
} else if (releaseFile.endsWith('.aab')) {
const res = await internalSharingUploadBundle(options, releaseFile)
core.setOutput("internalSharingDownloadUrl", res.downloadUrl);
core.exportVariable("INTERNAL_SHARING_DOWNLOAD_URL", res.downloadUrl);
console.log(`${releaseFile} uploaded to Internal Sharing, download it with ${res.downloadUrl}`)
return Promise.resolve(res.downloadUrl)
res = await internalSharingUploadBundle(options, releaseFile)
} else {
return Promise.reject(`${releaseFile} is invalid`)
throw Error(`${releaseFile} is invalid (missing or invalid file extension).`)
}
if (!res.downloadUrl) throw Error('Uploaded file has no download URL.')
core.setOutput("internalSharingDownloadUrl", res.downloadUrl);
core.exportVariable("INTERNAL_SHARING_DOWNLOAD_URL", res.downloadUrl);
console.log(`${releaseFile} uploaded to Internal Sharing, download it with ${res.downloadUrl}`)
return res.downloadUrl
}
async function uploadRelease(appEdit: AppEdit, 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);
return Promise.resolve(apk.versionCode);
} else if (releaseFile.endsWith('.aab')) {
const bundle = await uploadBundle(appEdit, options, releaseFile);
await uploadMappingFile(appEdit, 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<void> {
core.info(`Validating track '${options.track}'`)
const res = await androidPublisher.edits.tracks.list({
auth: options.auth,
editId: appEdit.id!,
editId: appEditId,
packageName: options.applicationId
});
// If we didn't get status 200, i.e. success, propagate the error with valid text
if (res.status != 200) {
throw Error(res.statusText)
}
const allTracks = res.data.tracks;
if (allTracks == undefined || allTracks.find(value => value.track == options.track) == undefined) {
return Promise.reject(`Track "${options.track}" could not be found `);
// Check whether we actually have any tracks
if (!allTracks) {
throw Error('No tracks found, unable to validate track.')
}
// Check whether the track is valid
if (allTracks.find(value => value.track == options.track) == undefined) {
const allTrackNames = allTracks.map((track) => { return track.track });
throw Error(`Track "${options.track}" could not be found. Available tracks are: ${allTrackNames.toString()}`);
}
}
async function addReleasesToTrack(appEdit: AppEdit, options: EditOptions, versionCodes: number[]): Promise<Track> {
let status: string | undefined = options.status;
if (!status) {
if (options.userFraction != undefined) {
status = 'inProgress';
} else {
status = 'completed';
}
}
async function addReleasesToTrack(appEditId: string, options: EditOptions, versionCodes: number[]): Promise<Track> {
const status = options.status
core.debug(`Creating release for:`);
core.debug(`edit=${appEditId}`)
core.debug(`track=${options.track}`)
if (options.userFraction) {
core.debug(`userFraction=${options.userFraction}`)
}
core.debug(`status=${status}`)
core.debug(`versionCodes=${versionCodes.toString()}`)
core.debug(`Creating Track Release for Edit(${appEdit.id}) 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 +209,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: {
@@ -197,6 +229,65 @@ async function uploadMappingFile(appEdit: AppEdit, versionCode: number, options:
}
}
async function uploadDebugSymbolsFile(appEditId: string, versionCode: number, options: EditOptions) {
if (options.debugSymbols != undefined && options.debugSymbols.length > 0) {
const fileStat = lstatSync(options.debugSymbols);
let data: Buffer | null = null;
if (fileStat.isDirectory()) {
data = await createDebugSymbolZipFile(options.debugSymbols);
}
if (data == null) {
data = readFileSync(options.debugSymbols);
}
if (data != null) {
core.debug(`[${appEditId}, versionCode=${versionCode}, packageName=${options.applicationId}]: Uploading Debug Symbols file @ ${options.debugSymbols}`);
await androidPublisher.edits.deobfuscationfiles.upload({
auth: options.auth,
packageName: options.applicationId,
editId: appEditId,
apkVersionCode: versionCode,
deobfuscationFileType: 'nativeCode',
media: {
mimeType: 'application/octet-stream',
body: Readable.from(data)
}
})
}
}
}
async function zipFileAddDirectory(root: JSZip | null, dirPath: string, rootPath: string, isRootRoot: boolean) {
if (root == null) return root;
const newRootPath = path.join(rootPath, dirPath);
const fileStat = lstatSync(newRootPath);
if (!fileStat.isDirectory()) {
const data = readFileSync(newRootPath);
root.file(dirPath, data);
return root;
}
const dir = fs.readdirSync(newRootPath);
const zipFolder = isRootRoot ? root : root.folder(dirPath);
for (let pathIndex = 0; pathIndex < dir.length; pathIndex++) {
const underPath = dir[pathIndex];
await zipFileAddDirectory(zipFolder, underPath, newRootPath, false);
}
return root;
}
async function createDebugSymbolZipFile(debugSymbolsPath: string) {
const zipFile = JSZip();
await zipFileAddDirectory(zipFile, ".", debugSymbolsPath, true);
return zipFile.generateAsync({ type: "nodebuffer" });
}
async function internalSharingUploadApk(options: EditOptions, apkReleaseFile: string): Promise<InternalAppSharingArtifact> {
core.debug(`[packageName=${options.applicationId}]: Uploading Internal Sharing APK @ ${apkReleaseFile}`);
@@ -227,13 +318,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 +334,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)
@@ -257,3 +348,70 @@ async function uploadBundle(appEdit: AppEdit, options: EditOptions, bundleReleas
return res.data
}
async function getOrCreateEdit(options: EditOptions): Promise<string> {
// If we already have an ID, just return that
if (options.existingEditId) {
return options.existingEditId
}
// Else attempt to create a new edit. This will throw if there is an issue
core.info(`Creating a new Edit for this release`)
const insertResult = await androidPublisher.edits.insert({
auth: options.auth,
packageName: options.applicationId
})
// If we didn't get status 200, i.e. success, propagate the error with valid text
if (insertResult.status != 200) {
throw Error(insertResult.statusText)
}
// If the result was successful but we have no ID, somethign went horribly wrong
if (!insertResult.data.id) {
throw Error('New edit has no ID, cannot continue.')
}
core.debug(`This new edit expires at ${String(insertResult.data.expiryTimeSeconds)}`)
// Return the new edit ID
return insertResult.data.id
}
async function uploadReleaseFiles(appEditId: string, options: EditOptions, releaseFiles: string[]): Promise<number[]> {
const versionCodes: number[] = []
// Upload all release files
for (const releaseFile of releaseFiles) {
core.info(`Uploading ${releaseFile}`)
let versionCode: number
if (releaseFile.endsWith('.apk')) {
// Upload APK, or throw when something goes wrong
const apk = await uploadApk(appEditId, options, releaseFile);
if (!apk.versionCode) throw Error('Failed to upload APK.')
versionCode = apk.versionCode
} else if (releaseFile.endsWith('.aab')) {
// Upload AAB, or throw when something goes wrong
const bundle = await uploadBundle(appEditId, options, releaseFile);
if (!bundle.versionCode) throw Error('Failed to upload bundle.')
versionCode = bundle.versionCode
} else {
// Throw if file extension is not right
throw Error(`${releaseFile} is invalid.`);
}
// Upload version code
await uploadMappingFile(appEditId, versionCode, options)
await uploadDebugSymbolsFile(appEditId, versionCode, options)
versionCodes.push(versionCode)
}
core.info(`Successfully uploaded ${versionCodes.length} artifacts`)
return versionCodes
}
function inferInternalSharingDownloadUrl(
applicationId: string,
versionCode: number
) {
return `https://play.google.com/apps/test/${applicationId}/${versionCode}`;
}

56
src/input-validation.ts Normal file
View File

@@ -0,0 +1,56 @@
import fg from "fast-glob";
export async function validateUserFraction(userFraction: number | undefined): Promise<void> {
if (userFraction != undefined) {
// If userFraction was set, perform basic validation
if (isNaN(userFraction)) {
return Promise.reject(new Error(`'userFraction' must be a number! Got ${userFraction}`))
}
if (userFraction >= 1 || userFraction <= 0) {
return Promise.reject(new Error(`'userFraction' must be between 0 and 1! Got ${userFraction}`))
}
}
}
export async function validateStatus(status: string | undefined, hasUserFraction: boolean): Promise<void> {
// If status was set, perform basic validation
if (status != 'completed' && status != 'inProgress' && status != 'halted' && status != 'draft') {
return Promise.reject(new Error(`Invalid status provided! Must be one of 'completed', 'inProgress', 'halted', 'draft'. Got ${status ?? "undefined"}`))
}
// Validate userFraction is correct for the given status
switch (status) {
case 'completed':
case 'draft':
if (hasUserFraction) {
return Promise.reject(new Error(`Status '${status}' does not support 'userFraction'`))
}
break
case 'halted':
case 'inProgress':
if (!hasUserFraction) {
return Promise.reject(new Error(`Status '${status}' requires a 'userFraction' to be set`))
}
break
}
}
export async function validateInAppUpdatePriority(inAppUpdatePriority: number | undefined): Promise<void> {
if (inAppUpdatePriority) {
if (inAppUpdatePriority < 0 || inAppUpdatePriority > 5) {
return Promise.reject(new Error('inAppUpdatePriority must be between 0 and 5, inclusive-inclusive'))
}
}
}
export async function validateReleaseFiles(releaseFiles: string[] | undefined): Promise<string[]> {
if (!releaseFiles) {
return Promise.reject(new Error(`You must provide 'releaseFiles' in your configuration`))
} else {
const files = await fg(releaseFiles)
if (!files.length) {
return Promise.reject(new Error(`Unable to find any release file matching ${releaseFiles.join(',')}`))
}
return files
}
}

View File

@@ -1,14 +1,11 @@
import * as core from '@actions/core';
import * as fs from "fs";
import fg from "fast-glob";
import {uploadToPlayStore} from "./edits";
import * as google from '@googleapis/androidpublisher';
import * as core from '@actions/core'
import * as fs from "fs"
import { runUpload } from "./edits"
import { validateInAppUpdatePriority, validateReleaseFiles, validateStatus, validateUserFraction } from "./input-validation"
import { unlink, writeFile } from 'fs/promises'
import pTimeout from 'p-timeout'
const auth = new google.auth.GoogleAuth({
scopes: ['https://www.googleapis.com/auth/androidpublisher']
});
async function run() {
export async function run() {
try {
const serviceAccountJson = core.getInput('serviceAccountJson', { required: false });
const serviceAccountJsonRaw = core.getInput('serviceAccountJsonPlainText', { required: false});
@@ -16,119 +13,113 @@ async function run() {
const releaseFile = core.getInput('releaseFile', { required: false });
const releaseFiles = core.getInput('releaseFiles', { required: false })
?.split(',')
?.filter(x => x !== '') || [];
?.filter(x => x !== '');
const releaseName = core.getInput('releaseName', { required: false });
const track = core.getInput('track', { required: true });
const inAppUpdatePriority = core.getInput('inAppUpdatePriority', { required: false });
const userFraction = core.getInput('userFraction', { required: false });
const userFraction = core.getInput('userFraction', { required: false })
const status = core.getInput('status', { required: false });
const whatsNewDir = core.getInput('whatsNewDirectory', { required: false });
const mappingFile = core.getInput('mappingFile', { required: false });
const debugSymbols = core.getInput('debugSymbols', { 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) {
console.log("No service account json key provided!");
core.setFailed("You must provide one of 'serviceAccountJson' or 'serviceAccountJsonPlainText' to use this action");
return;
}
await validateServiceAccountJson(serviceAccountJsonRaw, serviceAccountJson)
// If the user has provided the raw plain text via secrets, or w/e, then write to file and
// set appropriate env variable for the auth
if (serviceAccountJsonRaw) {
const serviceAccountFile = "./serviceAccountJson.json";
fs.writeFileSync(serviceAccountFile, serviceAccountJsonRaw, {
encoding: 'utf8'
});
// Insure that the api can find our service account credentials
core.exportVariable("GOOGLE_APPLICATION_CREDENTIALS", serviceAccountFile);
}
if (serviceAccountJson) {
// Insure that the api can find our service account credentials
core.exportVariable("GOOGLE_APPLICATION_CREDENTIALS", serviceAccountJson);
}
// Validate user fraction as a number, and within [0.0, 1.0]
let userFractionFloat: number | undefined = parseFloat(userFraction);
if (!isNaN(userFractionFloat)) {
if (userFractionFloat <= 0.0 || userFractionFloat >= 1.0) {
core.setFailed('A provided userFraction must be between 0.0 and 1.0, inclusive-inclusive');
return;
}
// Validate user fraction
let userFractionFloat: number | undefined
if (userFraction) {
userFractionFloat = parseFloat(userFraction)
} else {
userFractionFloat = undefined;
userFractionFloat = undefined
}
await validateUserFraction(userFractionFloat)
// Validate release status
await validateStatus(status, userFractionFloat != undefined && !isNaN(userFractionFloat))
// Validate the inAppUpdatePriority to be a valid number in within [0, 5]
let inAppUpdatePriorityInt: number | undefined = parseInt(inAppUpdatePriority);
if (!isNaN(inAppUpdatePriorityInt)) {
if (inAppUpdatePriorityInt < 0 || inAppUpdatePriorityInt > 5) {
core.setFailed('inAppUpdatePriority must be between 0 and 5, inclusive-inclusive');
return;
}
let inAppUpdatePriorityInt: number | undefined
if (inAppUpdatePriority) {
inAppUpdatePriorityInt = parseInt(inAppUpdatePriority)
} else {
inAppUpdatePriorityInt = undefined;
inAppUpdatePriorityInt = undefined
}
await validateInAppUpdatePriority(inAppUpdatePriorityInt)
// Check release files while maintaining backward compatibility
let validatedReleaseFiles: string[] = [];
if (releaseFiles.length == 0 && !releaseFile) {
core.setFailed(`You must provide either 'releaseFile' or 'releaseFiles' in your configuration.`);
return;
} else if (releaseFiles.length == 0 && releaseFile) {
core.warning(`WARNING!! 'releaseFile' is deprecated and will be removed in a future release. Please migrate to 'releaseFiles'.`);
core.debug(`Validating ${releaseFile} exists`)
if (!fs.existsSync(releaseFile)) {
core.setFailed(`Unable to find release file @ ${releaseFile}`);
return;
} else {
validatedReleaseFiles = [releaseFile];
}
} else if (releaseFiles.length > 0) {
core.debug(`Finding files ${releaseFiles.join(',')}`)
const files = await fg(releaseFiles);
if (!files.length) {
core.setFailed(`Unable to find any release file @ ${releaseFiles.join(',')}`);
return;
}
validatedReleaseFiles = files;
if (releaseFile) {
core.warning(`WARNING!! 'releaseFile' is deprecated and will be removed in a future release. Please migrate to 'releaseFiles'`)
}
const validatedReleaseFiles: string[] = await validateReleaseFiles(releaseFiles ?? [releaseFile])
if (whatsNewDir != undefined && whatsNewDir.length > 0 && !fs.existsSync(whatsNewDir)) {
core.setFailed(`Unable to find 'whatsnew' directory @ ${whatsNewDir}`);
return
core.warning(`Unable to find 'whatsnew' directory @ ${whatsNewDir}`);
}
if (mappingFile != undefined && mappingFile.length > 0 && !fs.existsSync(mappingFile)) {
core.setFailed(`Unable to find 'mappingFile' @ ${mappingFile}`);
return
core.warning(`Unable to find 'mappingFile' @ ${mappingFile}`);
}
const authClient = await auth.getClient();
if (debugSymbols != undefined && debugSymbols.length > 0 && !fs.existsSync(debugSymbols)) {
core.warning(`Unable to find 'debugSymbols' @ ${debugSymbols}`);
}
const result = await uploadToPlayStore({
auth: authClient,
applicationId: packageName,
track: track,
inAppUpdatePriority: inAppUpdatePriorityInt || 0,
userFraction: userFractionFloat,
status: status,
whatsNewDir: whatsNewDir,
mappingFile: mappingFile,
name: releaseName
}, validatedReleaseFiles);
console.log(`Finished uploading to the Play Store: ${result}`)
} catch (error: any) {
core.setFailed(error.message)
await pTimeout(
runUpload(
packageName,
track,
inAppUpdatePriorityInt,
userFractionFloat,
whatsNewDir,
mappingFile,
debugSymbols,
releaseName,
changesNotSentForReview,
existingEditId,
status,
validatedReleaseFiles
),
{
milliseconds: 3.6e+6
}
)
} catch (error: unknown) {
if (error instanceof Error) {
core.setFailed(error.message)
} else {
core.setFailed('Unknown error occurred.')
}
} finally {
if (core.getInput('serviceAccountJsonPlainText', { required: false})) {
// Cleanup our auth file that we created.
core.debug('Cleaning up service account json file');
fs.unlinkSync('./serviceAccountJson.json');
await unlink('./serviceAccountJson.json');
}
}
}
run();
async function validateServiceAccountJson(serviceAccountJsonRaw: string | undefined, serviceAccountJson: string | undefined): Promise<string | undefined> {
if (serviceAccountJson && serviceAccountJsonRaw) {
// If the user provided both, print a warning one will be ignored
core.warning('Both \'serviceAccountJsonPlainText\' and \'serviceAccountJson\' were provided! \'serviceAccountJson\' will be ignored.')
}
if (serviceAccountJsonRaw) {
// If the user has provided the raw plain text, then write to file and set appropriate env variable
const serviceAccountFile = "./serviceAccountJson.json";
await writeFile(serviceAccountFile, serviceAccountJsonRaw, {
encoding: 'utf8'
});
core.exportVariable("GOOGLE_APPLICATION_CREDENTIALS", serviceAccountFile)
} else if (serviceAccountJson) {
// If the user has provided the json path, then set appropriate env variable
core.exportVariable("GOOGLE_APPLICATION_CREDENTIALS", serviceAccountJson)
} else {
// If the user provided neither, fail and exit
return Promise.reject("You must provide one of 'serviceAccountJsonPlainText' or 'serviceAccountJson' to use this action")
}
}
void run();

View File

@@ -1,9 +1,9 @@
import * as core from '@actions/core';
import * as fs from "fs";
import * as path from "path";
import {readFileSync} from "fs";
import {androidpublisher_v3} from "@googleapis/androidpublisher";
import LocalizedText = androidpublisher_v3.Schema$LocalizedText;
import { readFile } from 'fs/promises';
export async function readLocalizedReleaseNotes(whatsNewDir: string | undefined): Promise<LocalizedText[] | undefined> {
core.debug(`Executing readLocalizedReleaseNotes`);
@@ -12,16 +12,16 @@ export async function readLocalizedReleaseNotes(whatsNewDir: string | undefined)
.filter(value => /whatsnew-((.*-.*)|(.*))\b/.test(value));
const pattern = /whatsnew-(?<local>(.*-.*)|(.*))/;
let localizedReleaseNotes: LocalizedText[] = [];
const localizedReleaseNotes: LocalizedText[] = [];
core.debug(`Found files: ${releaseNotes}`);
releaseNotes.forEach(value => {
core.debug(`Found files: ${releaseNotes.toString()}`);
for (const value of releaseNotes) {
const matches = value.match(pattern);
core.debug(`Matches for ${value} = ${matches}`);
if (matches != undefined && matches.length == 4) {
if (matches != null && matches.length == 4) {
core.debug(`Matches for ${value} = ${matches.toString()}`);
const lang = matches[1];
const filePath = path.join(whatsNewDir, value);
const content = readFileSync(filePath, 'utf-8');
const content = await readFile(filePath, 'utf-8');
if (content != undefined) {
core.debug(`Found localized 'whatsnew-*-*' for Lang(${lang})`);
@@ -33,7 +33,7 @@ export async function readLocalizedReleaseNotes(whatsNewDir: string | undefined)
)
}
}
});
}
return localizedReleaseNotes
}