mirror of
https://github.com/r0adkll/sign-android-release
synced 2026-09-23 13:18:38 +00:00
Initial Release
This commit is contained in:
@@ -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
|
### `alias`
|
||||||
```bash
|
|
||||||
$ npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
Build the typescript
|
**Required:** The alias of your signing key
|
||||||
```bash
|
|
||||||
$ npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the tests :heavy_check_mark:
|
### `keyStorePassword`
|
||||||
```bash
|
|
||||||
$ npm test
|
|
||||||
|
|
||||||
PASS ./index.test.js
|
**Required:** The password to your signing keystore
|
||||||
✓ throws invalid number (3ms)
|
|
||||||
✓ wait 500 ms (504ms)
|
|
||||||
✓ test runs (95ms)
|
|
||||||
|
|
||||||
...
|
### `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
|
## Example usage
|
||||||
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
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
uses: actions/typescript-action@releases/v1
|
uses: actions/sign-android-release@v1
|
||||||
with:
|
with:
|
||||||
milliseconds: 1000
|
releaseDirectory: /app/build/outputs/apk/release
|
||||||
```
|
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||||
|
alias: ${{ secrets.ALIAS }}
|
||||||
See the [actions tab](https://github.com/actions/javascript-action/actions) for runs of this action! :rocket:
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
## 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
|
|
||||||
```
|
|
||||||
+24
-6
@@ -1,10 +1,28 @@
|
|||||||
name: 'Your name here'
|
name: 'Drew Heavner'
|
||||||
description: 'Provide a description here'
|
description: 'An action to sign an Android release APK or AAB'
|
||||||
author: 'Your name or organization here'
|
author: 'r0adkll'
|
||||||
|
branding:
|
||||||
|
icon: 'edit'
|
||||||
|
color: 'orange'
|
||||||
inputs:
|
inputs:
|
||||||
myInput: # change this
|
releaseDirectory:
|
||||||
description: 'input description here'
|
description: 'The directory to find your release to sign'
|
||||||
default: 'default value if applicable'
|
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:
|
runs:
|
||||||
using: 'node12'
|
using: 'node12'
|
||||||
main: 'lib/main.js'
|
main: 'lib/main.js'
|
||||||
|
|||||||
Generated
+17
-12
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "node12-template-action",
|
"name": "sign-android-release",
|
||||||
"version": "0.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -9,6 +9,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.0.0.tgz",
|
||||||
"integrity": "sha512-aMIlkx96XH4E/2YZtEOeyrYQfhlas9jIRkfGPqMwXD095Rdkzo4lB6ZmbxPQSzD+e1M+Xsm98ZhuSMYGv/AlqA=="
|
"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": {
|
"@babel/code-frame": {
|
||||||
"version": "7.5.5",
|
"version": "7.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",
|
||||||
@@ -1080,9 +1085,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"commander": {
|
"commander": {
|
||||||
"version": "2.20.0",
|
"version": "2.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||||
"integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==",
|
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
@@ -2293,9 +2298,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"handlebars": {
|
"handlebars": {
|
||||||
"version": "4.1.2",
|
"version": "4.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.1.tgz",
|
||||||
"integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==",
|
"integrity": "sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"neo-async": "^2.6.0",
|
"neo-async": "^2.6.0",
|
||||||
@@ -4936,13 +4941,13 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"uglify-js": {
|
"uglify-js": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.8",
|
||||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.8.tgz",
|
||||||
"integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==",
|
"integrity": "sha512-XhHJ3S3ZyMwP8kY1Gkugqx3CJh2C3O0y8NPiSxtm1tyD/pktLAkFZsFGpuNfTZddKDQ/bbDBLAd2YyA1pbi8HQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"commander": "~2.20.0",
|
"commander": "~2.20.3",
|
||||||
"source-map": "~0.6.1"
|
"source-map": "~0.6.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+7
-6
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "typescript-action",
|
"name": "sign-android-release",
|
||||||
"version": "0.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "TypeScript template action",
|
"description": "GitHub action used to sign Android release packages",
|
||||||
"main": "lib/main.js",
|
"main": "lib/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
@@ -10,17 +10,18 @@
|
|||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/actions/typescript-action.git"
|
"url": "git+https://github.com/r0adkll/sign-android-release.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"actions",
|
"actions",
|
||||||
"node",
|
"node",
|
||||||
"setup"
|
"setup"
|
||||||
],
|
],
|
||||||
"author": "YourNameOrOrganization",
|
"author": "r0adkll",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.0.0"
|
"@actions/core": "^1.0.0",
|
||||||
|
"@actions/exec": "^1.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^24.0.13",
|
"@types/jest": "^24.0.13",
|
||||||
|
|||||||
@@ -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]
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
-6
@@ -1,16 +1,48 @@
|
|||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core';
|
||||||
import {wait} from './wait'
|
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() {
|
async function run() {
|
||||||
try {
|
try {
|
||||||
const ms = core.getInput('milliseconds');
|
const releaseDir = core.getInput('releaseDirectory');
|
||||||
console.log(`Waiting ${ms} milliseconds ...`)
|
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())
|
console.log(`Preparing to sign key @ ${releaseDir} with signing key`);
|
||||||
await wait(parseInt(ms, 10));
|
|
||||||
core.debug((new Date()).toTimeString())
|
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
core.setFailed(error.message);
|
core.setFailed(error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<string> {
|
||||||
|
|
||||||
|
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<string> {
|
||||||
|
core.debug("Signing AAB file");
|
||||||
|
await exec.exec(`jarsigner`, [
|
||||||
|
'-keystore', signingKeyFile,
|
||||||
|
'-storepass', keyStorePassword,
|
||||||
|
'-keypass', keyPassword,
|
||||||
|
aabFile,
|
||||||
|
alias
|
||||||
|
]);
|
||||||
|
|
||||||
|
return aabFile
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user