diff --git a/README.md b/README.md index 792067e1..ce73706e 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,47 @@ -# Create a JavaScript Action using TypeScript +# Sign Android Release Action -Use this template to bootstrap the creation of a JavaScript action.:rocket: +This action will help you sign an Android `.apk` or `.aab` (Android App Bundle) file for release. -This template includes compilication support, tests, a validation workflow, publishing, and versioning guidance. +## Inputs -If you are new, there's also a simpler introduction. See the [Hello World JavaScript Action](https://github.com/actions/hello-world-javascript-action) +### `releaseDirectory` -## Create an action from this template +**Required:** The relative directory path in your project where your Android release file will be located -Click the `Use this Template` and provide the new repo details for your action +### `signingKeyBase64` -## Code in Master +**Required:** The base64 encoded signing key used to sign your app -Install the dependencies -```bash -$ npm install -``` +### `alias` -Build the typescript -```bash -$ npm run build -``` +**Required:** The alias of your signing key -Run the tests :heavy_check_mark: -```bash -$ npm test +### `keyStorePassword` - PASS ./index.test.js - ✓ throws invalid number (3ms) - ✓ wait 500 ms (504ms) - ✓ test runs (95ms) +**Required:** The password to your signing keystore -... -``` +### `keyPassword` -## Change action.yml +**Required:** The private key password for your signing keystore -The action.yml contains defines the inputs and output for your action. +## Outputs -Update the action.yml with your name, description, inputs and outputs for your action. +### `signedReleaseFile` -See the [documentation](https://help.github.com/en/articles/metadata-syntax-for-github-actions) +The path to the signed release file from this action -## Change the Code +### ENV: `SIGNED_RELEASE_FILE` -Most toolkit and CI/CD operations involve async operations so the action is run in an async function. +This also set's an environment variable that points to the signed release file -```javascript -import * as core from '@actions/core'; -... - -async function run() { - try { - ... - } - catch (error) { - core.setFailed(error.message); - } -} - -run() -``` - -See the [toolkit documentation](https://github.com/actions/toolkit/blob/master/README.md#packages) for the various packages. - -## Publish to a distribution branch - -Actions are run from GitHub repos. We will create a releases branch and only checkin production modules (core in this case). - -Comment out node_modules in .gitignore and create a releases/v1 branch -```bash -# comment out in distribution branches -# node_modules/ -``` - -```bash -$ git checkout -b releases/v1 -$ git commit -a -m "prod dependencies" -``` - -```bash -$ npm prune --production -$ git add node_modules -$ git commit -a -m "prod dependencies" -$ git push origin releases/v1 -``` - -Your action is now published! :rocket: - -See the [versioning documentation](https://github.com/actions/toolkit/blob/master/docs/action-versioning.md) - -## Validate - -You can now validate the action by referencing the releases/v1 branch +## Example usage ```yaml -uses: actions/typescript-action@releases/v1 +uses: actions/sign-android-release@v1 with: - milliseconds: 1000 -``` - -See the [actions tab](https://github.com/actions/javascript-action/actions) for runs of this action! :rocket: - -## Usage: - -After testing you can [create a v1 tag](https://github.com/actions/toolkit/blob/master/docs/action-versioning.md) to reference the stable and tested action - -```yaml -uses: actions/typescript-action@v1 -with: - milliseconds: 1000 -``` + releaseDirectory: /app/build/outputs/apk/release + signingKeyBase64: ${{ secrets.SIGNING_KEY }} + alias: ${{ secrets.ALIAS }} + keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.KEY_PASSWORD }} +``` \ No newline at end of file diff --git a/action.yml b/action.yml index 5cfdde4b..07384091 100644 --- a/action.yml +++ b/action.yml @@ -1,10 +1,28 @@ -name: 'Your name here' -description: 'Provide a description here' -author: 'Your name or organization here' +name: 'Drew Heavner' +description: 'An action to sign an Android release APK or AAB' +author: 'r0adkll' +branding: + icon: 'edit' + color: 'orange' inputs: - myInput: # change this - description: 'input description here' - default: 'default value if applicable' + releaseDirectory: + description: 'The directory to find your release to sign' + required: true + signingKeyBase64: + description: 'The key used to sign your release in base64 encoded format' + required: true + alias: + description: 'The key alias' + required: true + keyStorePassword: + description: 'The password to the keystore' + required: true + keyPassword: + description: 'The password for the key' + required: true +outputs: + signedReleaseFile: + description: 'The signed release APK or AAB file' runs: using: 'node12' main: 'lib/main.js' diff --git a/package-lock.json b/package-lock.json index 3c556ab4..60c26f4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { - "name": "node12-template-action", - "version": "0.0.0", + "name": "sign-android-release", + "version": "1.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -9,6 +9,11 @@ "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.0.0.tgz", "integrity": "sha512-aMIlkx96XH4E/2YZtEOeyrYQfhlas9jIRkfGPqMwXD095Rdkzo4lB6ZmbxPQSzD+e1M+Xsm98ZhuSMYGv/AlqA==" }, + "@actions/exec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.1.tgz", + "integrity": "sha512-nvFkxwiicvpzNiCBF4wFBDfnBvi7xp/as7LE1hBxBxKG2L29+gkIPBiLKMVORL+Hg3JNf07AKRfl0V5djoypjQ==" + }, "@babel/code-frame": { "version": "7.5.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", @@ -1080,9 +1085,9 @@ } }, "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "optional": true }, @@ -2293,9 +2298,9 @@ "dev": true }, "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.1.tgz", + "integrity": "sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -4936,13 +4941,13 @@ "dev": true }, "uglify-js": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", - "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "version": "3.6.8", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.8.tgz", + "integrity": "sha512-XhHJ3S3ZyMwP8kY1Gkugqx3CJh2C3O0y8NPiSxtm1tyD/pktLAkFZsFGpuNfTZddKDQ/bbDBLAd2YyA1pbi8HQ==", "dev": true, "optional": true, "requires": { - "commander": "~2.20.0", + "commander": "~2.20.3", "source-map": "~0.6.1" } }, diff --git a/package.json b/package.json index 9e198988..eac9553d 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "typescript-action", - "version": "0.0.0", + "name": "sign-android-release", + "version": "1.0.0", "private": true, - "description": "TypeScript template action", + "description": "GitHub action used to sign Android release packages", "main": "lib/main.js", "scripts": { "build": "tsc", @@ -10,17 +10,18 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/actions/typescript-action.git" + "url": "git+https://github.com/r0adkll/sign-android-release.git" }, "keywords": [ "actions", "node", "setup" ], - "author": "YourNameOrOrganization", + "author": "r0adkll", "license": "MIT", "dependencies": { - "@actions/core": "^1.0.0" + "@actions/core": "^1.0.0", + "@actions/exec": "^1.0.1" }, "devDependencies": { "@types/jest": "^24.0.13", diff --git a/src/io-utils.ts b/src/io-utils.ts new file mode 100644 index 00000000..04f4b158 --- /dev/null +++ b/src/io-utils.ts @@ -0,0 +1,14 @@ +import {Dirent} from "fs"; + +const path = require('path'); +const fs = require('fs'); + +export function findReleaseFile(releaseDir: string): Dirent | undefined { + const releaseFiles = fs.readdirSync(releaseDir, {withFileTypes: true}) + .filter(item => !item.isDirectory()) + .filter(item => item.name.endsWith(".apk") || item.name.endsWith(".aab")); + + if (releaseFiles.length > 0) { + return releaseFiles[0] + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 191f3882..c3254e8d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,16 +1,48 @@ import * as core from '@actions/core'; import {wait} from './wait' +import {signAabFile, signApkFile} from "./signing"; +const path = require('path'); +const fs = require('fs'); +const io = require('io-utils'); +const sign = require('signing'); async function run() { try { - const ms = core.getInput('milliseconds'); - console.log(`Waiting ${ms} milliseconds ...`) + const releaseDir = core.getInput('releaseDirectory'); + const signingKeyBase64 = core.getInput('signingKeyBase64'); + const alias = core.getInput('alias'); + const keyStorePassword = core.getInput('keyStorePassword'); + const keyPassword = core.getInput('keyPassword'); - core.debug((new Date()).toTimeString()) - await wait(parseInt(ms, 10)); - core.debug((new Date()).toTimeString()) + console.log(`Preparing to sign key @ ${releaseDir} with signing key`); - core.setOutput('time', new Date().toTimeString()); + // 1. Find release file + const releaseFile = io.findReleaseFile(releaseDir); + if (releaseFile !== undefined) { + core.debug(`Found release to sign: ${releaseFile.name}`); + + // 2. Now that we have a release file, decode and save the signing key + const signingKey = path.join(releaseDir, 'signingKey.jks'); + fs.writeFileSync(signingKey, signingKeyBase64, 'base64'); + + // 3. Now zipalign the release file + const releaseFilePath = path.join(releaseDir, releaseFile.name); + let signedReleaseFile = ''; + if (releaseFile.name.endsWith('.apk')) { + signedReleaseFile = await signApkFile(releaseFilePath, signingKey, alias, keyStorePassword, keyPassword); + } else if (releaseFile.name.endsWith('.aab')) { + signedReleaseFile = await signAabFile(releaseFilePath, signingKey, alias, keyStorePassword, keyPassword); + } else { + core.error('No valid release file to sign, abort.'); + core.setFailed('No valid release file to sign.'); + } + + core.exportVariable("SIGNED_RELEASE_FILE", signedReleaseFile); + core.setOutput('signedReleaseFile', signedReleaseFile); + } else { + core.error("No release file (.apk or .aab) could be found. Abort.") + core.setFailed('No release file (.apk or .aab) could be found.'); + } } catch (error) { core.setFailed(error.message); } diff --git a/src/signing.ts b/src/signing.ts new file mode 100644 index 00000000..ae20b03a --- /dev/null +++ b/src/signing.ts @@ -0,0 +1,53 @@ +import * as exec from '@actions/exec'; +import * as core from '@actions/core'; + +export async function signApkFile( + apkFile: string, + signingKeyFile: string, + alias: string, + keyStorePassword: string, + keyPassword: string +): Promise { + + core.debug("Zipaligning APK file"); + + // Align the apk file + const alignedApkFile = apkFile.replace('.apk', '-aligned.apk'); + await exec.exec('zipalign', [ + '-v', '-p 4', + apkFile, + alignedApkFile + ]); + + core.debug("Signing APK file"); + + // apksigner sign --ks my-release-key.jks --out my-app-release.apk my-app-unsigned-aligned.apk + const signedApkFile = apkFile.replace('.apk', '-signed.apk'); + await exec.exec('apksigner', [ + 'sign', + '--ks', signingKeyFile, + '--out', signedApkFile, + alignedApkFile + ]); + + return signedApkFile +} + +export async function signAabFile( + aabFile: string, + signingKeyFile: string, + alias: string, + keyStorePassword: string, + keyPassword: string +): Promise { + core.debug("Signing AAB file"); + await exec.exec(`jarsigner`, [ + '-keystore', signingKeyFile, + '-storepass', keyStorePassword, + '-keypass', keyPassword, + aabFile, + alias + ]); + + return aabFile +} \ No newline at end of file