Skip to content

Commit 7078216

Browse files
authored
further refactoring of code and tests (#233)
1 parent 1014a8f commit 7078216

File tree

9 files changed

+345
-242
lines changed

9 files changed

+345
-242
lines changed

.github/workflows/test-release.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,8 @@ jobs:
219219
github_token: ${{ secrets.GITHUB_TOKEN }}
220220
release_branches: main
221221
pre_release_branches: dev
222+
fetch_all_tags: true
223+
default_bump: minor
222224
- name: Create a GitHub release
223225
uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1
224226
if: github.ref == 'refs/heads/main'

README.md

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ jobs:
7676
permissions: "actions:write,issues:read"
7777

7878
- name: Use Application Token to checkout a repository
79-
uses: actions/checkout@v3
79+
uses: actions/checkout@v4
8080
env:
8181
GITHUB_TOKEN: ${{ steps.get_workflow_token.outputs.token }}
8282
with:
@@ -99,7 +99,7 @@ jobs:
9999
organization: CattleDip
100100

101101
- name: Use Application Token to checkout a repository
102-
uses: actions/checkout@v3
102+
uses: actions/checkout@v4
103103
env:
104104
GITHUB_TOKEN: ${{ steps.get_workflow_token.outputs.token }}
105105
with:
@@ -116,14 +116,26 @@ inputs:
116116
application_id:
117117
description: GitHub Application ID value.
118118
required: true
119+
application_installation_id:
120+
description: GitHub Install Application ID value.
121+
required: false
119122
permissions:
120123
description: "The permissions to request e.g. issues:read,secrets:write,packages:read. Defaults to all available permissions"
121124
required: false
122-
organization:
123-
description: The GitHub Organization to get the application installation for, if not specified will use the current repository instead
125+
org:
126+
description: The GitHub Organisation to get the application installation for, if not specified will use the current repository instead. This is not normally needed as the workflow will be running in the context of a repository / org.
127+
required: false
128+
owner:
129+
description: The GitHub Owner to get the application installation for, if not specified will use the current repository instead. This is not normally needed as the workflow will be running in the context of a repository / org.
130+
required: false
131+
repo:
132+
description: The GitHub Repository to get the application installation for, if not specified will use the current repository instead (owner must also be specified). This is not normally needed as the workflow will be running in the context of a repository / org.
124133
required: false
125134
github_api_base_url:
126-
description: The GitHub API base URL to use, no needed it working within the same GitHub instance as the workflow as it will get picked up from the environment
135+
description: The GitHub API base URL to use, no needed it working within the same GitHub instance as the workflow as it will get picked up from the environment. This not usually needed and is mainly for testing purposes.
136+
required: false
137+
token_lifetime:
138+
description: The lifetime of the token in seconds, defaults to 600 seconds (10 minutes).
127139
required: false
128140
```
129141
@@ -133,12 +145,12 @@ inputs:
133145
outputs:
134146
token:
135147
description: A valid token representing the Application that can be used to access what the Application has been scoped to access.
148+
expires_at:
149+
description: The date and time when the token will expire (UTC).
136150
permissions_requested:
137-
description: The permissions that were requested for the token, if not specified will be your default permissions.
151+
description: The permissions that were requested for the token.
138152
permissions_granted:
139153
description: The permissions that were granted for the token.
140-
expires_at:
141-
description: The date and time that the token will expire in UTC.
142154
```
143155
144156
## Requirements

action.yml

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,28 @@ inputs:
2020
description: "The permissions to request e.g. issues:read,secrets:write,packages:read. Defaults to all available permissions"
2121
required: false
2222

23-
organization:
24-
description: The GitHub Organization to get the application installation for, if not specified will use the current repository instead
23+
org:
24+
description: The GitHub Organisation to get the application installation for, if not specified will use the current repository instead. This is not normally needed as the workflow will be running in the context of a repository / org.
25+
required: false
26+
27+
owner:
28+
description: The GitHub Owner to get the application installation for, if not specified will use the current repository instead. This is not normally needed as the workflow will be running in the context of a repository / org.
29+
required: false
30+
31+
repo:
32+
description: The GitHub Repository to get the application installation for, if not specified will use the current repository instead (owner must also be specified). This is not normally needed as the workflow will be running in the context of a repository / org.
2533
required: false
2634

2735
github_api_base_url:
28-
description: The GitHub API base URL to use, no needed it working within the same GitHub instance as the workflow as it will get picked up from the environment
36+
description: The GitHub API base URL to use, no needed it working within the same GitHub instance as the workflow as it will get picked up from the environment. This not usually needed and is mainly for testing purposes.
37+
required: false
38+
39+
token_lifetime:
40+
description: The lifetime of the token in seconds, defaults to 600 (10 minutes).
41+
required: false
42+
43+
debug:
44+
description: Enable debug logging.
2945
required: false
3046

3147
outputs:

dist/index.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

index.js

Lines changed: 121 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,133 @@
11
import process from 'process';
2+
import { Buffer } from 'buffer';
23
import { getOctokit } from '@actions/github';
34
import jwt from 'jsonwebtoken';
45
import * as core from '@actions/core';
56
import { getInstallationId } from './lib/github-application.js';
67

8+
/**
9+
* Test if the data is a valid RSA private key, decode it if it's base64 encoded and return the key
10+
* @param {string} data RSA private key in PEM format or base64 encoded
11+
* @returns {string} RSA private key (decoded if data is base64 encoded)
12+
* @throws {Error} If the data is not a valid RSA private key
13+
* @example const privateKey = decodePrivateKey('LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQ==');
14+
*/
15+
export function decodePrivateKey(data) {
16+
/**
17+
* @param {string} data
18+
**/
19+
if (!data) {
20+
const message = 'Invalid RSA private key';
21+
core.error(message);
22+
throw new Error(message);
23+
}
24+
25+
const privateKey = data.replace(/\\n/g, '\n').trim();
26+
const decoded = Buffer.from(privateKey, 'base64').toString('ascii');
27+
try {
28+
jwt.decode(decoded, { complete: true });
29+
return decoded;
30+
} catch (error) {
31+
return privateKey;
32+
}
33+
}
34+
35+
/**
36+
* Convert the raw permissions string to an object
37+
* @param {string} permissionsRaw The raw permissions string
38+
* @returns {object} The permissions object
39+
* @example const permissions = permissionsRawToObj('contents:read,actions:read');
40+
*/
41+
function permissionsRawToObj(permissionsRaw) {
42+
return permissionsRaw.split(',').reduce((acc, permission) => {
43+
const [key, value] = permission.split(':');
44+
acc[key] = value;
45+
return acc;
46+
}, {});
47+
}
48+
49+
/**
50+
* Get the input from the workflow file
51+
* @returns {object} The input from the workflow file or the environment variables as a fallback
52+
* @throws {Error} If the required inputs are missing
53+
*/
754
export function getInput() {
855
if (process.env.GITHUB_ACTIONS) {
956
const privateKey = core.getInput('application_private_key', { required: true });
1057
const appId = core.getInput('application_id', { required: true });
1158
const permissionsRaw = core.getInput('permissions');
59+
const org = core.getInput('org', { required: false });
60+
const owner = core.getInput('owner', { required: false });
61+
const repo = core.getInput('repo', { required: false });
62+
const baseApiUrl = core.getInput('base_api_url', { required: false });
63+
const debug = core.getInput('debug', { required: false });
1264
// convert the string (e.g. "contents:read,actions:read" to an object, e.g. { contents: 'read', actions: 'read' })
13-
const permissionsInput = permissionsRaw
14-
? permissionsRaw.split(',').reduce((acc, permission) => {
15-
const [key, value] = permission.split(':');
16-
acc[key] = value;
17-
return acc;
18-
}, {})
19-
: {};
65+
const permissionsInput = permissionsRaw ? permissionsRawToObj(permissionsRaw) : undefined;
66+
const tokenLifetime = core.getInput('token_lifetime', { required: false }) ?? 600;
2067

21-
return { privateKey, appId, permissionsInput };
68+
return {
69+
privateKey,
70+
appId,
71+
permissionsInput,
72+
org,
73+
owner,
74+
repo,
75+
baseApiUrl,
76+
debug,
77+
tokenLifetime,
78+
};
2279
} else {
2380
if (!process.env.GITHUB_APPLICATION_PRIVATE_KEY || !process.env.GITHUB_APPLICATION_ID) {
81+
core.error('Required inputs are missing: privateKey or appId is undefined');
2482
throw new Error('Required inputs are missing');
2583
}
2684
const privateKey = process.env.GITHUB_APPLICATION_PRIVATE_KEY.replace(/\\n/g, '\n');
2785
const appId = process.env.GITHUB_APPLICATION_ID;
28-
const permissionsRaw = process.env.GITHUB_PERMISSIONS ?? '{}';
29-
const permissionsInput = permissionsRaw
30-
? permissionsRaw.split(',').reduce((acc, permission) => {
31-
const [key, value] = permission.split(':');
32-
acc[key] = value;
33-
return acc;
34-
}, {})
35-
: {};
36-
return { privateKey, appId, permissionsInput };
86+
const org = process.env.GITHUB_APPLICATION_ORG ?? undefined;
87+
const owner = process.env.GITHUB_APPLICATION_OWNER ?? undefined;
88+
const repo = process.env.GITHUB_APPLICATION_REPO ?? undefined;
89+
const baseApiUrl = process.env.GITHUB_APPLICATION_BASE_API_URL ?? undefined;
90+
const debug = process.env.DEBUG ?? false;
91+
const permissionsRaw = process.env.GITHUB_APPLICATION_PERMISSIONS ?? undefined;
92+
const permissionsInput = permissionsRaw ? permissionsRawToObj(permissionsRaw) : undefined;
93+
const tokenLifetime = process.env.GITHUB_APPLICATION_TOKEN_LIFETIME ?? 600;
94+
95+
return {
96+
privateKey,
97+
appId,
98+
permissionsInput,
99+
org,
100+
owner,
101+
repo,
102+
baseApiUrl,
103+
debug,
104+
tokenLifetime,
105+
};
37106
}
38107
}
39108

109+
/**
110+
* Main function
111+
*/
40112
export async function run() {
113+
if (process.env.DEBUG) {
114+
core.setCommandEcho(true);
115+
}
116+
41117
try {
42-
// get the variables from the if/else block
43118
const { privateKey, appId, permissionsInput } = getInput();
44-
45-
if (!privateKey || !appId) {
46-
throw new Error('Required inputs are missing: privateKey or appId is undefined');
47-
}
48-
49119
const permissions = permissionsInput;
50-
51-
// Ensure getInstallationId is correctly awaited and check its result before proceeding
52120
const installationId = await getInstallationId(privateKey, appId);
53-
if (!installationId) {
54-
throw new Error('Failed to retrieve installation ID');
55-
}
56-
// console.debug('Retrieved Installation ID:', installationId);
57-
58121
const jwtToken = generateJwtToken(privateKey, appId);
59122
const octokit = getOctokit(jwtToken);
60123

124+
if (!privateKey || !appId) {
125+
const message = 'Required inputs are missing: privateKey or appId is undefined';
126+
core.error(message);
127+
throw new Error(message);
128+
}
129+
130+
// create an installation access token
61131
const {
62132
data: { token },
63133
} = await octokit.rest.apps.createInstallationAccessToken({
@@ -72,22 +142,41 @@ export async function run() {
72142

73143
const permissionsGranted = installation.data.permissions;
74144

145+
// set the Github Actions outputs
146+
core.startGroup('GitHub App Installation');
75147
core.setSecret(token);
148+
core.setSecret(jwtToken);
149+
core.setSecret(`${installationId}`);
76150
core.setOutput('token', token);
77151
core.setOutput('expires_at', new Date().toISOString());
78152
core.setOutput('permissions_requested', JSON.stringify(permissions));
79153
core.setOutput('permissions_granted', JSON.stringify(permissionsGranted));
154+
core.endGroup();
80155
} catch (error) {
81-
console.error('Error:', error.message);
156+
const message = 'Error getting installation access token';
157+
core.error(message, error.message);
158+
console.error(message, error.message);
82159
core.setFailed(error.message);
83160
}
84161
}
85162

163+
/**
164+
* @param {jwt.Secret} privateKey
165+
* @param {any} appId
166+
*/
86167
export function generateJwtToken(privateKey, appId) {
87168
const iat = Math.floor(Date.now() / 1000);
88169
const exp = iat + 10 * 60; // JWT expiration time set to 10 minutes
89170
const payload = { iat, exp, iss: appId };
90-
return jwt.sign(payload, privateKey, { algorithm: 'RS256' });
171+
try {
172+
return jwt.sign(payload, privateKey, { algorithm: 'RS256' });
173+
} catch (error) {
174+
const message = 'Error generating JWT token';
175+
core.error(message, error.message);
176+
console.error(message, error.message);
177+
throw new Error(message);
178+
}
91179
}
92180

181+
// The run function is the entry point for the GitHub Action
93182
run();

0 commit comments

Comments
 (0)