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
|
||||
```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 }}
|
||||
```
|
||||
+24
-6
@@ -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'
|
||||
|
||||
Generated
+17
-12
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
+7
-6
@@ -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",
|
||||
|
||||
@@ -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 {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);
|
||||
}
|
||||
|
||||
@@ -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